Получить доступ

Collectors и свёртка

reduce против collect, базовые коллекторы и ловушка toMap, группировка и downstream-коллекторы, характеристики и параллельность.

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) как замену ручному циклу и упомяните характеристики/ассоциативность для параллельной сборки.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем