reduce против collect
Свёртка стрима бывает двух видов. reduce — неизменяемая свёртка: identity + ассоциативная функция объединения (1+2+3 → сумма). Она хороша для значений (числа, конкатенация коротких строк), но накапливать ею в коллекцию неэффективно — на каждом шаге создаётся новый контейнер.
collect — изменяемая свёртка (mutable reduction): результат накапливается в один мутируемый контейнер (список, карта, StringBuilder) через Collector. Для построения коллекций используют именно collect.
Базовые коллекторы и ловушка toMap
Collectors.toList()/toSet()/toUnmodifiableList() собирают в коллекцию; joining(", ") — в строку с разделителем. toMap(keyFn, valueFn) собирает в Map.
Ловушка toMap: если два элемента дают одинаковый ключ, бросается IllegalStateException (Duplicate key). Поэтому при возможных дубликатах ОБЯЗАТЕЛЬНО передают третий аргумент — функцию слияния (merge), решающую конфликт (например, (a, b) -> a или сумму).
Map<String, Integer> byName = users.stream()
.collect(Collectors.toMap(User::name, User::age,
(a, b) -> a)); // merge: что делать при дубликате ключа
Группировка
groupingBy(classifier) превращает поток в Map<K, List<V>> — группы по ключу (заказы по пользователю, слова по длине). partitioningBy(predicate) — частный случай: разбивает на два ведра по булеву условию (Map<Boolean, List<V>>), всегда содержит оба ключа.
Это декларативная замена ручному циклу с computeIfAbsent.
Map<Department, List<Employee>> byDept =
employees.stream().collect(Collectors.groupingBy(Employee::department));
Downstream-коллекторы
Сила groupingBy — во втором аргументе, downstream-коллекторе, который сворачивает каждую группу дальше: counting() (размер группы), mapping(fn, toList()) (преобразовать элементы группы), summingInt/averagingInt (агрегаты), toSet(), maxBy/minBy.
Так одной строкой строят «количество сотрудников по отделам» или «средняя зарплата по отделам» — вложенная свёртка.
Map<Department, Long> headcount = employees.stream()
.collect(Collectors.groupingBy(Employee::department, Collectors.counting()));
Характеристики и параллельность
У коллектора есть характеристики (CONCURRENT, UNORDERED, IDENTITY_FINISH), подсказывающие стриму, можно ли собирать конкурентно и важен ли порядок. Для параллельной группировки есть groupingByConcurrent.
Главное практическое следствие — функции коллектора должны быть ассоциативны и без побочных эффектов, иначе параллельная свёртка даст неверный результат. И помните про обязательный merge в toMap для устойчивости к дубликатам.
Как отвечать на собеседовании
Различите reduce (immutable, для значений) и collect (mutable reduction, для контейнеров), назовите базовые коллекторы и обязательно — ловушку toMap без merge (IllegalStateException на дубликате). Покажите groupingBy с downstream (counting/mapping/summingInt) как замену ручному циклу и упомяните характеристики/ассоциативность для параллельной сборки.