Чтобы сгруппировать данные по какому-нибудь признаку, нам надо использовать в связке метод 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.