Статьи

Окончательное руководство по Java Stream API groupingBy () Collector

GroupingBy () является одним из самых мощных и настраиваемых потока API коллекторов.

Если вы постоянно обнаруживаете, что не выходите за рамки следующего использования   groupingBy():

.collect(groupingBy(...));

Или, если вы просто хотели обнаружить его потенциальное использование, то эта статья для вас!

обзор

Проще говоря, groupingBy () предоставляет функциональность, аналогичную предложению SQL GROUP BY, только для API Java Stream .

Чтобы использовать его, нам всегда нужно указывать свойство, по которому будет выполняться группировка. Мы делаем это, предоставляя реализацию функционального интерфейса — обычно передавая лямбда-выражение.

Например, если мы хотим , чтобы группы строк по их длине, мы могли бы сделать , что, переходя   String::length к   groupingBy():

List<String> strings = List.of("a", "bb", "cc", "ddd"); 

Map<Integer, List<String>> result = strings.stream() 
  .collect(groupingBy(String::length)); 

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}

Но сам коллектор способен делать гораздо больше, чем простые группировки, как показано выше.

Группировка в реализацию пользовательской карты

Если вам нужно предоставить пользовательскую  Map реализацию, вы можете сделать это, используя предоставленную   groupingBy() перегрузку:

List<String> strings = List.of("a", "bb", "cc", "ddd");

TreeMap<Integer, List<String>> result = strings.stream()
  .collect(groupingBy(String::length, TreeMap::new, toList()));

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}

Предоставление пользовательской нисходящей коллекции

Если вам нужно хранить сгруппированные элементы в пользовательской коллекции, это можно сделать с помощью   toCollection()  сборщика.

Например, если вы хотите сгруппировать элементы в  TreeSet экземплярах, это может быть так просто:

groupingBy(String::length, toCollection(TreeSet::new))

И вот, у нас есть полный пример:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, TreeSet<String>> result = strings.stream()
  .collect(groupingBy(String::length, toCollection(TreeSet::new)));

System.out.println(result); // {1=[a], 2=[bb, cc], 3=[ddd]}

Группировка и подсчет предметов в группах

Если вы просто хотите узнать количество сгруппированных элементов, это может быть так же просто, как и предоставить собственный  counting() сборщик:

groupingBy(String::length, counting())

Вот полный пример:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Long> result = strings.stream()
  .collect(groupingBy(String::length, counting()));

System.out.println(result); // {1=1, 2=2, 3=1}

Группировка и объединение элементов в виде строк

Если вам нужно сгруппировать элементы и создать одно строковое представление каждой группы, это можно сделать с помощью  joining() сборщика:

groupingBy(String::length, joining(",", "[", "]"))

И, чтобы увидеть это в действии, посмотрите следующий пример:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, String> result = strings.stream()
  .collect(groupingBy(String::length, joining(",", "[", "]")));

System.out.println(result); // {1=[a], 2=[bb,cc], 3=[ddd]}

Группировка и фильтрация товаров

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

groupingBy(String::length, filtering(s -> !s.contains("c"), toList()))

Вот этот коллекционер в действии:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, List<String>> result = strings.stream()
  .collect(groupingBy(String::length, filtering(s -> !s.contains("c"), toList())));

System.out.println(result); // {1=[a], 2=[bb], 3=[ddd]}

Группировка и расчет среднего по группе

Если необходимо получить среднее значение свойств сгруппированных элементов, для этого есть несколько удобных сборщиков:

  •  averagingInt() 
  •  averagingLong() 
  •  averagingDouble() 

Вот эти коллекционеры в действии:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Double> result = strings.stream()
  .collect(groupingBy(String::length, averagingInt(String::hashCode)));

System.out.println(result); // {1=97.0, 2=3152.0, 3=99300.0}

Отказ от ответственности:   String::hashCode был использован в качестве заполнителя.

Группировка и расчет суммы на группу

Если вы хотите получить сумму из свойств сгруппированных элементов, для этого также есть несколько вариантов:

  •  summingInt() 
  •  summingLong() 
  •  summingDouble() 

Вот эти коллекционеры в действии:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Integer> result = strings.stream()
  .collect(groupingBy(String::length, summingInt(String::hashCode)));

System.out.println(result); // {1=97, 2=6304, 3=99300}

Отказ от ответственности:   String::hashCode был использован в качестве заполнителя.

Группировка и расчет статистической сводки по группе

Если вы хотите сгруппировать, а затем получить статистическую сводку по свойствам сгруппированных элементов, для этого также есть готовые опции:

  •  summarizingInt() 
  •  summarizingLong() 
  •  summarizingDouble() 

Вот вышеупомянутые коллекционеры в действии:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, IntSummaryStatistics> result = strings.stream()
  .collect(groupingBy(String::length, summarizingInt(String::hashCode)));

System.out.println(result);

Давайте посмотрим на результат (удобный переформатированный):

{
    1=IntSummaryStatistics{
      count=1, 
      sum=97, 
      min=97, 
      average=97.000000, 
      max=97}, 
    2=IntSummaryStatistics{
      count=2, 
      sum=6304, 
      min=3136, 
      average=3152.000000, 
      max=3168}, 
    3=IntSummaryStatistics{
      count=1, 
      sum=99300, 
      min=99300, 
      average=99300.000000, 
      max=99300}
}

Отказ от ответственности:   String::hashCode был использован в качестве заполнителя.

Группировка и сокращение элементов

Если вы хотите выполнить операцию сокращения для сгруппированных элементов, вы можете использовать  reducing()коллектор:

groupingBy(List::size, reducing(List.of(), (l1, l2) -> ...)))

Вот пример  reducing() коллектора:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, List<Character>> result = strings.stream()
  .map(toStringList())
  .collect(groupingBy(List::size, reducing(List.of(), (l1, l2) -> Stream.concat(l1.stream(), l2.stream())
    .collect(Collectors.toList()))));

System.out.println(result); // {1=[a], 2=[b, b, c, c], 3=[d, d, d]}

Группировка и расчет Max / Min Item

Если вы хотите извлечь элемент max / min из группы, вы можете просто использовать   max()/ min()collector:

groupingBy(String::length, Collectors.maxBy(Comparator.comparing(String::toUpperCase)))

Вот эти коллекционеры в действии:

List<String> strings = List.of("a", "bb", "cc", "ddd");

Map<Integer, Optional<String>> result = strings.stream()
  .collect(groupingBy(String::length, Collectors.maxBy(Comparator.comparing(String::toUpperCase))));

System.out.println(result); // {1=Optional[a], 2=Optional[cc], 3=Optional[ddd]}

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

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

Составление нижестоящих коллекторов

Вся мощь коллектора раскрывается, как только мы начинаем объединять несколько коллекторов для определения сложных операций группирования в нисходящем направлении, которые начинают напоминать стандартные конвейеры Stream API — это предел для неба.

Пример № 1

Допустим, у нас есть список строк и мы хотим получить карту длин строк, связанных со строками в верхнем регистре, длиной больше единицы, и собрать их в  TreeSet  экземпляр.

Мы можем сделать это довольно легко:

var result = strings.stream()
  .collect(
    groupingBy(String::length,
      mapping(String::toUpperCase,
        filtering(s -> s.length() > 1,
          toCollection(TreeSet::new)))));

//result
{1=[], 2=[BB, CC], 3=[DDD]}

Пример № 2

Имея список строк, сгруппируйте их по их совпадающим длинам, преобразуйте их в список символов, сгладьте полученный список, сохраните только отдельные элементы с ненулевой длиной и, в конечном итоге, уменьшите их, применяя объединение строк.

Мы можем достичь этого также:

var result = strings.stream()
  .collect(
    groupingBy(String::length,
      mapping(toStringList(),
        flatMapping(s -> s.stream().distinct(),
          filtering(s -> s.length() > 0,
            mapping(String::toUpperCase,
              reducing("", (s, s2) -> s + s2)))))
    ));

//result 
{1=A, 2=BC, 3=D}

источники

Все приведенные выше примеры можно найти в моем проекте GitHub.


Если вам понравилась эта статья и вы хотите узнать больше о потоках Java, ознакомьтесь с этой коллекцией руководств и статей  по всем вопросам, связанным с потоками Java.