Что делает операция collect()?
Самый частый случай — сборка в List:
🟢 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 функций:
supplier()— создает новый контейнер (ArrayList::new)accumulator()— добавляет элемент в контейнер (List::add)combiner()— объединяет два контейнера (нужно дляparallelStream)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
- Простой подсчёт — используйте
count()вместоcollect(toList()).size() - Проверка существования —
anyMatch()вместоcollect(toList())и проверки isEmpty() - Поиск одного элемента —
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()]]