Группировка элементов коллекции без Stream API

Рейтинг: 3Ответов: 1Опубликовано: 07.03.2023

Есть метод, который группирует элементы коллекции из нескольким полям.

public void test() {
    List<Item> itemsList = ...//Чтение из XML;

    Map<List<Object>, List<Item>> groupBy =
            itemsList.stream().collect(Collectors.groupingBy(
                    i -> Arrays.asList(i.id, i.city, i.order_status)));

    for (Map.Entry<List<Object>, List<Item>> t : groupBy.entrySet()) {
        System.out.println(t.getKey());
        for (Item item : t.getValue()) {
            System.out.println("    " + item);
        }
    }
}

А как сделать такое же без Stream API?

Ответы

▲ 2Принят

Просто создается мапа с явным указанием типа ключа и значений, в цикле проходишь по списку и добавляешь элементы в мапу при помощи Map::computeIfAbsent -- этот метод при необходимости создаёт новое или возвращает существующее значение-список, для которого можно сразу же вызвать метод List:add.
При этом лучше использовать LinkedHashMap, чтобы порядок элементов в ней был стабильный (HashMap порядка не гарантирует).

public static Map<List<Object>, List<Item>> groupBy(List<Item> list) {
    Map<List<Object>, List<Item>> map = new LinkedHashMap<>();
    for (Item item : list) {
        List<Object> key = Arrays.asList(item.getId(), item.getCity(), item.getOrderStatus());
        map.computeIfAbsent(key, k -> new ArrayList<>()).add(item);
    }
    return map;
}

Следует отметить, что вместо списка объектов правильнее было бы использовать некий промежуточный класс, для которого нужно корректно переопределить методы hashCode и equals. Это достаточно просто сделать при помощи кортежей record, официально поддерживаемых с Java 16, или Lombok-аннотаций @Data / @AllArgsConstructor.

enum OrderStatus {NEW, PROCESSING, COMPLETED, CANCELED};

// определены конструктор, геттеры без префикса `get`, hashCode / equals, toString
record MyKey(long id, String city, OrderStatus status) {
    public MyKey(Item item) {
        this(item.getId(), item.getCity(), item.getOrderStatus());
    }
}

public static Map<MyKey, List<Item>> groupBy(List<Item> list) {
    Map<MyKey, List<Item>> map = new LinkedHashMap<>();
    list.forEach(item -> map
        .computeIfAbsent(new MyKey(item), k -> new ArrayList<>())
        .add(item)
    );

    // отладочный вывод
    map.forEach((key, val) -> {
        System.out.println(key);
        val.forEach(item -> System.out.println("\t" + item));
    });

    return map;
}

При желании создание такой группировки можно параметризовать для любого ключа, если передавать некую функцию-конвертор Function<Item, Key>, в последнем примере такой функцией был конструктор кортежа MyKey(Item item). Например, для генерации ключа-списка можно было бы определить отдельный фабричный метод:

// MyClass
public static List<Object> keyIdCityStatus(Item item) {
    return Arrays.asList(item.getId(), item.getCity(), item.getOrderStatus());
}

Параметризованный метод выглядит так:

public static<K> Map<K, List<Item>> groupBy(Function<Item, K> keyBuilder, List<Item> list) {
    Map<K, List<Item>> map = new LinkedHashMap<>();

    list.forEach(item -> map
        .computeIfAbsent(keyBuilder.apply(item), k -> new ArrayList<>())
        .add(item)
    );

    return map;
}

Соответственно вызывать его можно для любого ключа:

// ссылка на фабричный метод MyClass::keyIdCityStatus
Map<List<Object>, List<Item>> keyListMap = groupBy(MyClass::keyIdCityStatus, list);

// группировка по двум полям с помощью лямбды
Map<List<Object>, List<Item>> byIdAndStatus = groupBy(
    item -> List.of(item.getId(), item.getOrderStatus()), 
    list
);

// ссылка на конструктор MyKey::new
Map<MyKey, List<Item>> keyRecordMap = groupBy(MyKey::new, list);