Группировка
Группировка раскладывает элементы по корзинам: все заказы одного покупателя — в одну, все слова одной длины — в другую. В Stream API для этого есть коллектор Collectors.groupingBy. Результат — словарь, где ключ — признак группы, а значение — её элементы.
groupingBy
Сгруппируем слова по длине. Функция, переданная в groupingBy, вычисляет ключ группы для каждого элемента:
void main() {
List<String> words = List.of("кот", "лес", "мост", "сад", "дорога", "луна");
Map<Integer, List<String>> byLength = words.stream()
.collect(Collectors.groupingBy(w -> w.length()));
IO.println(byLength);
}Результат:
{3=[кот, лес, сад], 4=[мост, луна], 6=[дорога]}Каждое значение словаря — список слов этой длины, порядок внутри списка такой же, как в исходных данных. Тип результата — Map<Integer, List<String>>.
Что делать с группой
Вторым аргументом groupingBy принимает ещё один коллектор — он решает, что сделать с элементами каждой группы. Его называют нижестоящим (downstream). Например, вместо списков можно сразу посчитать количество или сумму:
record Order(String customer, int amount) {}
void main() {
List<Order> orders = List.of(
new Order("Анна", 20), new Order("Олег", 35),
new Order("Анна", 15), new Order("Ира", 50));
Map<String, Long> count = orders.stream()
.collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.counting()));
Map<String, Integer> total = orders.stream()
.collect(Collectors.groupingBy(Order::customer, TreeMap::new, Collectors.summingInt(Order::amount)));
IO.println(count);
IO.println(total);
}Результат:
{Анна=2, Ира=1, Олег=1}
{Анна=35, Ира=50, Олег=35}Средний аргумент TreeMap::new задаёт тип словаря. TreeMap хранит ключи отсортированными, поэтому имена идут по алфавиту.
mapping
Коллектор Collectors.mapping преобразует элементы группы перед сборкой. Так можно собрать не заказы целиком, а только суммы:
record Order(String customer, int amount) {}
void main() {
List<Order> orders = List.of(
new Order("Анна", 20), new Order("Олег", 35), new Order("Анна", 15));
Map<String, List<Integer>> amounts = orders.stream()
.collect(Collectors.groupingBy(
Order::customer,
TreeMap::new,
Collectors.mapping(Order::amount, Collectors.toList())));
IO.println(amounts);
}Результат:
{Анна=[20, 15], Олег=[35]}partitioningBy
partitioningBy — частный случай группировки: всего две группы, true и false. Ключом служит результат условия:
void main() {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
Map<Boolean, List<Integer>> parts = numbers.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
IO.println("Чётные: " + parts.get(true));
IO.println("Нечётные: " + parts.get(false));
}Результат:
Чётные: [2, 4, 6]
Нечётные: [1, 3, 5, 7]В отличие от groupingBy, обе группы есть в словаре всегда — даже если одна из них пустая.
Обход результата
Результат группировки — обычный словарь, его можно обойти через entrySet() или forEach:
void main() {
List<String> fruits = List.of("яблоко", "груша", "ананас", "голубика", "абрикос");
fruits.stream()
.collect(Collectors.groupingBy(f -> f.charAt(0), TreeMap::new, Collectors.toList()))
.forEach((letter, list) -> IO.println(letter + ": " + list));
}Результат:
а: [ананас, абрикос]
г: [груша, голубика]
я: [яблоко]Частая ошибка
По умолчанию groupingBy складывает группы в HashMap, а он не обещает никакого порядка ключей. Код может годами выводить группы в «правильном» порядке, а после смены данных или версии Java — перемешать их:
void main() {
List<String> names = List.of("Олег", "Анна", "Ира", "Борис", "Вера", "Алла");
IO.println(names.stream().collect(Collectors.groupingBy(n -> n.substring(0, 2))));
}Результат (порядок ключей не гарантирован):
{Ве=[Вера], Ир=[Ира], Ал=[Алла], Бо=[Борис], Ан=[Анна], Ол=[Олег]}Если порядок важен, передавайте фабрику словаря явно: TreeMap::new для сортировки по ключу или LinkedHashMap::new для порядка появления.