Статьи

Java 8 Streams API: группировка и разбиение потока

В этом посте показано, как можно использовать Collectors доступные в Streams API, для группировки элементов потока с помощью groupingBy и элементов разделения потока с помощью partitioningBy .

Рассмотрим поток объектов Employee , каждый с именем, городом и количеством продаж, как показано в таблице ниже:

1
2
3
4
5
6
7
8
+----------+------------+-----------------+
| Name     | City       | Number of Sales |
+----------+------------+-----------------+
| Alice    | London     | 200             |
| Bob      | London     | 150             |
| Charles  | New York   | 160             |
| Dorothy  | Hong Kong  | 190             |
+----------+------------+-----------------+

Группировка

Давайте начнем с группировки сотрудников по городам с использованием императивного стиля (pre-lamba) Java:

01
02
03
04
05
06
07
08
09
10
Map<String, List<Employee>> result = new HashMap<>();
for (Employee e : employees) {
  String city = e.getCity();
  List<Employee> empsInCity = result.get(city);
  if (empsInCity == null) {
    empsInCity = new ArrayList<>();
    result.put(city, empsInCity);
  }
  empsInCity.add(e);
}

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

В Java 8 вы можете сделать то же самое с помощью одного оператора, используя сборщик groupingBy , например так:

1
2
Map<String, List<Employee>> employeesByCity =
  employees.stream().collect(groupingBy(Employee::getCity));

Это приводит к следующей карте:

1
{New York=[Charles], Hong Kong=[Dorothy], London=[Alice, Bob]}

Кроме того, можно подсчитать количество сотрудников в каждом городе, передав коллектор counting groupingBy . Второй коллектор выполняет дальнейшую операцию сокращения для всех элементов в потоке, классифицированных в той же группе.

1
2
Map<String, Long> numEmployeesByCity =
  employees.stream().collect(groupingBy(Employee::getCity, counting()));

В результате получается следующая карта:

1
{New York=1, Hong Kong=1, London=2}

Кроме того, это эквивалентно следующему оператору SQL:

1
select city, count(*) from Employee group by city

Другим примером является вычисление среднего количества продаж в каждом городе, что можно сделать с помощью коллектора averagingInt в сочетании с коллектором groupingBy :

1
2
3
Map<String, Double> avgSalesByCity =
  employees.stream().collect(groupingBy(Employee::getCity,
                               averagingInt(Employee::getNumSales)));

В результате получается следующая карта:

1
{New York=160.0, Hong Kong=190.0, London=175.0}

Разметка

Разделение — это особый вид группировки, в которой результирующая карта содержит не более двух разных групп — одну для true и одну для false . Например, если вы хотите выяснить, кто ваши лучшие сотрудники, вы можете разделить их на тех, кто сделал больше, чем N продаж, и тех, кто этого не сделал, используя сборщик partitioningBy :

1
2
Map<Boolean, List<Employee>> partitioned =
  employees.stream().collect(partitioningBy(e -> e.getNumSales() > 150));

Это даст следующий результат:

1
{false=[Bob], true=[Alice, Charles, Dorothy]}

Вы также можете объединить разделение и группировку, передав сборщик groupingBy сборщик partitioningBy . Например, вы можете посчитать количество сотрудников в каждом городе в каждом разделе:

1
2
3
Map<Boolean, Map<String, Long>> result =
  employees.stream().collect(partitioningBy(e -> e.getNumSales() > 150,
                               groupingBy(Employee::getCity, counting())));

Это создаст двухуровневую карту:

1
{false={London=1}, true={New York=1, Hong Kong=1, London=1}}
Ссылка: Java 8 Streams API: группировка и разбиение потока от нашего партнера по JCG Фахда Шарифа в блоге fahd.blog .