Чтобы сгруппировать данные по какому-нибудь признаку, нам надо использовать в связке метод collect() объекта Stream и метод
Collectors.groupingBy(). Допустим, у нас есть следующий класс:
class Phone{
private String name;
private String company;
private int price;
public Phone(String name, String comp, int price){
this.name=name;
this.company=comp;
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() имеет похожее действие, только он делит элементы на группы по принципу, соответствует ли элемент определенному условию. Например:
Map<Boolean, List<Phone>> phonesByCompany = phoneStream.collect(
Collectors.partitioningBy(p->p.getCompany()=="Apple"));
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->p.getCompany()=="Apple" мы смотрим, принадлежит ли телефон компании Apple. Если телефон принадлежит этой
компании, то он попадает в одну группу, если нет, то в другую.
Метод 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 применяется для подсчета суммы. В зависимости от типа данных, к которым применяется метод, он имеет следующие формы:
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 применяются для подсчета минимального и максимального значения в каждой группе. В качестве параметра эти методы принимают функцию компаратора, которая нужна для сравнения значений. Например, найдем для каждой компании телефон с минимальной ценой:
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>.
Методы 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 позволяет дополнительно обработать данные и задать функцию отображения объектов из потока на какой-нибудь другой тип данных. Например:
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.