Перейти к основному содержимому

Группировка

Чтобы сгруппировать данные по какому-нибудь признаку, нам надо использовать в связке метод collect() объекта Stream и метод Collectors.groupingBy(). Допустим, у нас есть следующий класс:

class Phone {
private String name;
private String company;
private int price;

public Phone(String name, String company, int price) {
this.name = name;
this.company = company;
this.price = price;
}

public String getName() {
return name;
}

public int getPrice() {
return price;
}

public String getCompany() {
return company;
}
}

И, к примеру, у нас есть набор объектов Phone, которые мы хотим сгруппировать по компаниям:

import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.stream.Collectors;

public class Program {
public static void main(String[] args) {
Stream<Phone> phoneStream = Stream.of(
new Phone("iPhone X", "Apple", 600),
new Phone("Pixel 2", "Google", 500),
new Phone("iPhone 8", "Apple",450),
new Phone("Galaxy S9", "Samsung", 440),
new Phone("Galaxy S8", "Samsung", 340)
);

Map<String, List<Phone>> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany)
);

for (Map.Entry<String, List<Phone>> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey());
for (Phone phone : item.getValue()) {
System.out.println(phone.getName());
}
System.out.println();
}
}
}

Консольный вывод:

Google
Pixel 2

Apple
iPhone X
iPhone 8

Samsung
Galaxy S9
Galaxy S8

Итак, для создания групп в метод phoneStream.collect() передаётся вызов функции Collectors.groupingBy(), которая с помощью выражения Phone::getCompany группирует объекты по компаниям. В итоге будет создан объект Map, в котором ключами являются названия компаний, а значениями - список связанных с компаниями телефонов.

Метод Collectors.partitioningBy​

Метод Collectors.partitioningBy() имеет похожее действие, только он делит элементы на группы по принципу, соответствует ли элемент определенному условию. Например:

Map<Boolean, List<Phone>> phonesByCompany = phoneStream.collect(
Collectors.partitioningBy(p -> "Apple".equals(p.getCompany()))
);

for (Map.Entry<Boolean, List<Phone>> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey());
for (Phone phone : item.getValue()) {
System.out.println(phone.getName());
}
System.out.println();
}

В данном случае с помощью условия p -> "Apple".equals(p.getCompany()) мы смотрим, принадлежит ли телефон компании Apple. Если телефон принадлежит этой компании, то он попадает в одну группу, если нет, то в другую.

Метод Coollectors.counting​

Метод Collectors.counting применяется в Collectors.groupingBy() для вычисления количества элементов в каждой группе:

Map<String, Long> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany, Collectors.counting())
);

for (Map.Entry<String, Long> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue());
}

Консольный вывод:

Google - 1
Apple - 2
Samsung - 2

Метод Collectors.summing​

Метод Collectors.summing применяется для подсчёта суммы. В зависимости от типа данных, к которым применяется метод, он имеет следующие формы: summingInt(), summingLong(), summingDouble(). Применим этот метод для подсчета стоимости всех смартфонов по компаниям:

Map<String, Integer> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany, Collectors.summingInt(Phone::getPrice))
);

for (Map.Entry<String, Integer> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue());
}

С помощью выражения Collectors.summingInt(Phone::getPrice)) мы указываем, что для каждой компании будет вычисляться совокупная цена всех её смартфонов. И поскольку вычисляется результат - сумма для значений типа int, то в качестве типа возвращаемой коллекции используется тип Map<String, Integer>.

Консольный вывод:

Google - 500
Apple - 1050
Samsung - 780

Методы maxBy и minBy​

Методы maxBy и minBy применяются для подсчёта минимального и максимального значения в каждой группе. В качестве параметра эти методы принимают функцию компаратора, которая нужна для сравнения значений. Например, найдем для каждой компании телефон с минимальной ценой:

Map<String, Optional<Phone>> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany, Collectors.minBy(Comparator.comparing(Phone::getPrice)))
);

for (Map.Entry<String, Optional<Phone>> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue().get().getName());
}

Консольный вывод:

Google - Pixel 2
Apple - iPhone 8
Samsung - Galaxy S8

В качестве возвращаемого значения операции группировки используется объект Map<String, Optional<Phone>>. Опять же поскольку группируем по компаниям, то ключом будет выступать строка, а значением - объект Optional<Phone>.

Метод summarizing​

Методы summarizingInt() / summarizingLong() / summarizingDouble() позволяют объединить в набор значения соответствующих типов:

import java.util.IntSummaryStatistics;
//....................................

Map<String, IntSummaryStatistics> priceSummary = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany, Collectors.summarizingInt(Phone::getPrice))
);

for (Map.Entry<String, IntSummaryStatistics> item : priceSummary.entrySet()) {
System.out.println(item.getKey() + " - " + item.getValue().getAverage());
}

Метод Collectors.summarizingInt(Phone::getPrice)) создает набор, в который помещаются цены для всех телефонов каждой из групп. Данный набор инкапсулируется в объекте IntSummaryStatistics. Соответственно если бы мы применяли методы summarizingLong() или summarizingDouble(), то соответственно бы получали объекты LongSummaryStatistics или DoubleSummaryStatistics.

У этих объектов есть ряд методов, который позволяют выполнить различные атомарные операции над набором:

  • getAverage(): возвращает среднее значение.
  • getCount(): возвращает количество элементов в наборе.
  • getMax(): возвращает максимальное значение.
  • getMin(): возвращает минимальное значение.
  • getSum(): возвращает сумму элементов.
  • accept(): добавляет в набор новый элемент.

В данном случае мы получаем среднюю цену смартфона для каждой группы.

Консольный вывод:

Google - 500.0
Apple - 525.0
Samsung - 390.0

Метод mapping​

Метод mapping позволяет дополнительно обработать данные и задать функцию отображения объектов из потока на какой-нибудь другой тип данных. Например:

Map<String, List<String>> phonesByCompany = phoneStream.collect(
Collectors.groupingBy(Phone::getCompany, Collectors.mapping(Phone::getName, Collectors.toList()))
);

for (Map.Entry<String, List<String>> item : phonesByCompany.entrySet()) {
System.out.println(item.getKey());
for (String name : item.getValue()) {
System.out.println(name);
}
}

Выражение Collectors.mapping(Phone::getName, Collectors.toList()) указывает, что в группу будут выделяться названия смартфонов, причем группа будет представлять объект List.