Вопрос 5 · Раздел 8

Что делает операция collect()?

Самый частый случай — сборка в List:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

collect() — это терминальная (финальная) операция, которая запускает весь конвейер стрима и упаковывает результаты в нужную вам структуру: List, Map, String и т.д.

«Терминальная» означает: после collect() стрим исчерпан, его нельзя использовать повторно. «Собирает» = складывает элементы в контейнер по заданному правилу.

Самый частый случай — сборка в List:

List<String> result = stream.collect(Collectors.toList());

// В Set (удаляет дубликаты)
Set<String> unique = stream.collect(Collectors.toSet());

// В строку с разделителем
String joined = stream.collect(Collectors.joining(", "));

// Подсчёт элементов
long count = stream.collect(Collectors.counting());

Важно: collect() запускает выполнение всех промежуточных операций.

🟡 Middle Level

Анатомия Collector<T, A, R>

Коллектор состоит из 4 функций:

  1. supplier() — создает новый контейнер (ArrayList::new)
  2. accumulator() — добавляет элемент в контейнер (List::add)
  3. combiner() — объединяет два контейнера (нужно для parallelStream)
  4. finisher() — финальное преобразование

Мутабельная редукция

Мутабельная редукция — накопление результата в изменяемый контейнер (ArrayList, StringBuilder). В отличие от немутабельной (reduce), где каждый шаг создаёт новый объект. Мутабельная эффективнее по памяти.

В отличие от reduce(), который создает новый объект на каждом шаге, collect() модифицирует существующий контейнер. Это гораздо эффективнее для коллекций.

GroupingBy — “SQL внутри Java”

// Группировка по городу
Map<City, List<Person>> byCity = persons.stream()
    .collect(groupingBy(Person::getCity));

// Группировка с подсчётом
Map<City, Long> countByCity = persons.stream()
    .collect(groupingBy(Person::getCity, counting()));

// Группировка с вложенным коллектором
Map<City, Map<String, List<Person>>> complex = persons.stream()
    .collect(groupingBy(Person::getCity, groupingBy(Person::getGender)));

ToMap с разрешением конфликтов

Всегда используйте версию с тремя аргументами:

.collect(toMap(User::getId, u -> u, (existing, replacement) -> existing));

Без merge function при коллизии получите IllegalStateException.

Когда НЕ использовать collect

  1. Простой подсчёт — используйте count() вместо collect(toList()).size()
  2. Проверка существованияanyMatch() вместо collect(toList()) и проверки isEmpty()
  3. Поиск одного элементаfindFirst() / findAny() вместо collect + get(0)

🔴 Senior Level

Характеристики коллекторов (Characteristics)

  • IDENTITY_FINISH — метод finisher можно пропустить
  • UNORDERED — порядок элементов не важен (быстрее в параллели)
  • CONCURRENT — контейнер потокобезопасен (ConcurrentHashMap), позволяет нескольким потокам писать в один контейнер без combiner()

Parallel Stream Combiner

При написании кастомного коллектора никогда не игнорируйте combiner(). Даже если сейчас не используете parallelStream(), кто-то может вызвать его позже — код сломается.

Производительность

Не используйте reduce() для накопления коллекций: reduce с мутабельным контейнером (new ArrayList) сломает параллельный режим — один и тот же список используется во всех ветках. collect создаёт отдельный контейнер для каждой ветки.

// ПЛОХО — O(n²), копирует весь список на каждом шаге
stream.reduce(new ArrayList<>(), (list, e) -> { list.add(e); return list; }, ...)

// ХОРОШО — O(n), добавляет в существующий контейнер
stream.collect(toList())

Immutable Collections: Java 16+ имеет метод .toList() напрямую у стрима — он эффективнее и возвращает неизменяемый список.

Edge Cases

  • Null Values: Некоторые коллекторы (toMap, TreeMap) выбрасывают NPE при null ключах или значениях
  • Memory Consumption: Сборка 1 млн объектов — момент максимального потребления памяти

Диагностика

Профилируйте метод accumulator в кастомных коллекторах — это самая часто вызываемая часть (Hot Path).


🎯 Шпаргалка для интервью

Обязательно знать:

  • collect() — терминальная операция, запускает весь pipeline и упаковывает результат в структуру данных
  • Collector состоит из 4 функций: supplier, accumulator, combiner, finisher
  • Мутабельная редукция эффективнее immutable — модифицирует контейнер, а не создаёт новый объект
  • groupingBy — мощнейший инструмент: группировка, подсчёт, вложенные агрегации
  • toMap всегда использовать с merge function — иначе IllegalStateException при дубликатах
  • Java 16+: stream.toList() предпочтительнее — лаконичнее, возвращает неизменяемый список

Частые уточняющие вопросы:

  • collect vs reduce для коллекций? — collect эффективнее: reduce с mutable контейнером сломает параллельный режим
  • Что такое Characteristics? — Флаги оптимизации: IDENTITY_FINISH, UNORDERED, CONCURRENT
  • Когда collect() НЕ использовать? — Для простого подсчёта (count()), проверки существования (anyMatch()), поиска одного элемента (findFirst())
  • Что делает combiner? — Объединяет два контейнера, критичен для parallelStream

Красные флаги (НЕ говорить):

  • «collect можно вызвать несколько раз на одном стриме» — нет, стрим исчерпан после terminal операции
  • «reduce с new ArrayList так же хорош, как collect» — нет, O(n²) и ломает параллелизм
  • «toMap без merge function — это ок» — нет, краш при дубликатах ключей
  • «combiner не нужен, если не использую parallelStream» — кто-то вызовет его позже, код сломается

Связанные темы:

  • [[6. Что такое Collector и какие есть встроенные Collectors]]
  • [[2. В чём разница между intermediate и terminal операциями]]
  • [[9. Что такое параллельные стримы]]
  • [[3. Что делает операция filter()]]