Различия между Collectors.toMap() и Collectors.groupingBy() для сбора в карту

Я хочу создать Map из List из Points и иметь внутри карты все записи из списка, сопоставленные с одним и тем же parentId, например Map<Long, List<Point>>.
Я использовал Collectors.toMap(), но он не компилируется:

Map<Long, List<Point>> pointByParentId = chargePoints.stream()
    .collect(Collectors.toMap(Point::getParentId, c -> c));

person Tim Schwalbe    schedule 21.07.2017    source источник
comment
Похоже, вы ищете Collectors.groupingBy.   -  person Louis Wasserman    schedule 21.07.2017
comment
Я согласен с интерпретацией вопроса Луи Вассерманом, но вы должны быть более четкими в своем вопросе. Предоставьте несколько примеров ввода и вывода или даже минимальный, полный и проверяемый пример.   -  person Robin Topper    schedule 21.07.2017


Ответы (4)


TLDR:

Чтобы собрать Map, содержащий одно значение по ключу (Map<MyKey,MyObject>), используйте Collectors.toMap().
Чтобы собрать в Map несколько значений по ключу (Map<MyKey, List<MyObject>>), используйте Collectors.groupingBy().


Коллекторы.toMap()

Написав:

chargePoints.stream().collect(Collectors.toMap(Point::getParentId, c -> c));

Возвращенный объект будет иметь тип Map<Long,Point>.
Посмотрите на функцию Collectors.toMap(), которую вы используете:

Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper)

Он возвращает Collector с результатом Map<K,U>, где K и U — это тип возврата двух функций, переданных в метод. В вашем случае Point::getParentId — это Long, а c относится к Point. Принимая во внимание, что Map<Long,Point> возвращается, когда применяется collect().

И такое поведение скорее ожидается, поскольку Collectors.toMap() в javadoc указано:

возвращает Collector, который накапливает элементы в Map, чьи ключи и значения являются результатом применения предоставленных функций отображения к входным элементам.

Но если сопоставленные ключи содержат дубликаты (согласно Object.equals(Object)), выдается IllegalStateException
Вероятно, это будет ваш случай, поскольку вы группируете Point в соответствии с определенным свойством: parentId.

Если сопоставленные ключи могут иметь дубликаты, вы можете использовать toMap(Function, Function, BinaryOperator) перегружает, но на самом деле это не решит вашу проблему, так как не будет группировать элементы с тот же parentId. Это просто даст возможность не иметь двух элементов с одним и тем же parentId.


Коллекторы.groupingBy()

Чтобы выполнить ваше требование, вы должны использовать Collectors.groupingBy(), поведение и объявление метода которого лучше подходят для ваших нужд:

public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) 

Он указывается как:

Возвращает Collector, реализующий операцию «группировать по» для входных элементов типа T, группируя элементы в соответствии с функцией классификации и возвращая результаты в Map.

Метод принимает Function.
В вашем случае параметр Function равен Point (type Stream), и вы возвращаете Point.getParentId(), так как хотите сгруппировать элементы по parentId значениям.

Итак, вы могли бы написать:

Map<Long, List<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy( p -> p.getParentId())); 

Или со ссылкой на метод:

Map<Long, List<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy(Point::getParentId));

Collectors.groupingBy(): идем дальше

Действительно, сборщик groupingBy() идет дальше, чем реальный пример. Наконец, метод Collectors.groupingBy(Function<? super T, ? extends K> classifier) — это просто удобный метод для хранения значений собранных Map в List.
Чтобы сохранить значения Map в чем-то другом, кроме List, или для сохранения результата определенного вычисления, groupingBy(Function<? super T, ? extends K> classifier, Collector<? super T, A, D> downstream) должен вас заинтересовать. .

Например :

Map<Long, Set<Point>> pointByParentId = 
                       chargePoints.stream()
                                   .collect(Collectors.groupingBy(Point::getParentId, toSet()));

Таким образом, помимо заданного вопроса, вы должны рассматривать groupingBy() как гибкий способ выбора значений, которые вы хотите сохранить в собранном Map, чем окончательно toMap() не является.

person davidxxx    schedule 21.07.2017
comment
@TimSchwalbe в переполнении стека вместо того, чтобы благодарить, мы голосуем и принимаем ответы. - person xenteros; 21.07.2017
comment
Поиск информации о toMap()! Поздороваться! - person GhostCat; 19.09.2018

Collectors.groupingBy - это именно то, что вам нужно, он создает карту из вашей входной коллекции, создавая запись, используя Function, который вы предоставляете для своего ключа, и список точек с вашим связанным ключом в качестве его значения.

Map<Long, List<Point>> pointByParentId = chargePoints.stream()
    .collect(Collectors.groupingBy(Point::getParentId));
person Patrick    schedule 21.07.2017
comment
документация: docs.oracle.com/javase/8/docs/api/java/util/stream/ - person Matt C.; 09.06.2021

Следующий код делает это. Collectors.toList() используется по умолчанию, поэтому вы можете его пропустить, но если вы хотите иметь Map<Long, Set<Point>>, потребуется Collectors.toSet().

Map<Long, List<Point>> map = pointList.stream()
                .collect(Collectors.groupingBy(Point::getParentId, Collectors.toList()));
person xenteros    schedule 21.07.2017

Довольно часто верно, что карту из object.field в коллекцию объектов, которые совместно используют это поле, лучше хранить в Multimap (у Guava есть хорошая реализация для multimap). Если вам НЕ НУЖНО, чтобы мультикарта была изменчивой (что должно быть желаемым случаем), вы можете использовать

Multimaps.index(chargePoints, Point::getParentId);

Если вам необходимо использовать изменяемую карту, вы можете либо реализовать сборщик (как показано здесь: https://blog.jayway.com/2014/09/29/java-8-collector-for-gauvas-linkedhashmultimap/) или используйте цикл for (или forEach) для заполнения пустой изменяемой мультикарты.

Мультикарта дает вам дополнительные функции, которые вам обычно нужны, когда вы используете карту из поля для набора объектов, совместно использующих поле (например, подсчет общего количества объектов).

Изменяемая мультикарта также упрощает добавление и удаление элементов на карте (не заботясь о пограничных случаях).

person Roy Shahaf    schedule 16.12.2018