Что такое Collector и какие есть встроенные Collectors?
Пример использования:
🟢 Junior Level
Collector — это рецепт для collect(). Он описывает четыре шага: как создать контейнер (supplier), как добавить элемент (accumulator), как объединить два контейнера (combiner), и как преобразовать результат (finisher).
Основные встроенные Collectors:
// В список
Collectors.toList()
// В множество (удаляет дубликаты)
Collectors.toSet()
// В мапу
Collectors.toMap(keyMapper, valueMapper)
// Группировка
Collectors.groupingBy(classifier)
// Строка-соединение
Collectors.joining(", ")
// Подсчёт
Collectors.counting()
// Сумма
Collectors.summingInt(User::getAge)
// Среднее
Collectors.averagingInt(User::getAge)
Пример использования:
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
🟡 Middle Level
Анатомия Collector<T, A, R>
supplier()— создает аккумулятор (ArrayList::new)accumulator()— добавляет элемент (List::add)combiner()— объединяет аккумуляторы (дляparallelStream)finisher()— финальное преобразованиеcharacteristics()— флаги оптимизации
Характеристики Collector:
- CONCURRENT — accumulator можно вызывать из разных потоков (быстрее в parallelStream)
- UNORDERED — порядок элементов не важен (меньше синхронизации при merge)
- IDENTITY_FINISH — finisher не нужен, контейнер = результат (меньше overhead)
Продвинутые Collectors
GroupingBy с Downstream:
// Группировка + подсчёт
Map<City, Long> countByCity = persons.stream()
.collect(groupingBy(Person::getCity, counting()));
// Группировка + агрегация
Map<Department, Double> avgSalary = employees.stream()
.collect(groupingBy(Employee::getDept, averagingDouble(Employee::getSalary)));
// Partitioning — разделение на true/false
Map<Boolean, List<User>> partitioned = users.stream()
.collect(partitioningBy(User::isActive));
ToMap ловушка коллизий:
// ПЛОХО — бросит IllegalStateException при дубликатах
.toMap(User::getId, u -> u)
// ХОРОШО — с merge function
.toMap(User::getId, u -> u, (old, replacement) -> old)
Teeing (Java 12+):
// Два коллектора + объединение результатов
var result = stream.collect(teeing(
minBy(Comparator.naturalOrder()),
maxBy(Comparator.naturalOrder()),
(min, max) -> new Range(min, max)
));
Когда НЕ использовать кастомный Collector
- Простое накопление в List/Set — используйте
toList(),toSet()(Java 16+:toList()immutable) - Группировка с одним уровнем —
groupingBy()покрывает 95% случаев - Ваш Collector сложнее, чем альтернатива — иногда два прохода проще
🔴 Senior Level
Characteristics флаги
CONCURRENT— аккумулятор потокобезопасен. В параллельном стриме потоки пишут в один экземпляр, пропуская дорогойcombiner().UNORDERED— коллектор не сохраняет порядок (быстрее дляSetи параллельной группировки)IDENTITY_FINISH— результат аккумуляции кастится напрямую кR, минуяfinisher()
GroupingByConcurrent
Для параллельных стримов используйте groupingByConcurrent — использует ConcurrentMap и флаг CONCURRENT, что на огромных данных работает в разы быстрее обычного groupingBy.
Производительность
Immutable Collectors: toUnmodifiableList() (Java 10) эффективнее, чем collect(toList()) с последующим оборачиванием.
Custom Collector Cost: accumulator вызывается для каждого элемента. Любая лишняя аллокация — смерть для GC под нагрузкой.
toList() vs collect(Collectors.toList()): Начиная с Java 16, stream.toList() предпочтительнее — лаконичнее, возвращает неизменяемый список, оптимизирован внутри.
Диагностика
Если collect тормозит в параллельных стримах — проверьте combiner. Плохая реализация (list1.addAll(list2)) может свести на нет все преимущества параллелизма.
🎯 Шпаргалка для интервью
Обязательно знать:
- Collector — «рецепт» для collect(): supplier, accumulator, combiner, finisher + characteristics
- Characteristics: CONCURRENT (потокобезопасный accumulator), UNORDERED (без сохранения порядка), IDENTITY_FINISH (finisher не нужен)
- Основные встроенные: toList, toSet, toMap, groupingBy, partitioningBy, joining, counting, summingXxx, averagingXxx
groupingByс downstream — группировка + подсчёт/агрегация в одном проходе- Teeing (Java 12+) — два коллектора + объединение результатов
- groupingByConcurrent для параллельных стримов — быстрее обычного groupingBy
Частые уточняющие вопросы:
- toMap без merge function — что будет? — IllegalStateException при дубликате ключа
- Когда кастомный Collector НЕ нужен? — Простое накопление (toList/toSet), группировка (groupingBy покрывает 95%)
- Чем CONCURRENT коллектор лучше? — В parallelStream потоки пишут в один контейнер, минуя дорогой combiner
- toList() vs collect(Collectors.toList())? — Java 16+: toList() лаконичнее, возвращает immutable список
Красные флаги (НЕ говорить):
- «Collector хранит данные» — нет, это только описание правил сборки
- «combiner можно игнорировать» — нет, parallelStream сломается
- «groupingBy всегда медленный» — нет, groupingByConcurrent решает проблему параллелизма
- «IDENTITY_FINISH означает, что finisher вызывается один раз» — нет, он не вызывается вообще
Связанные темы:
- [[5. Что делает операция collect()]]
- [[9. Что такое параллельные стримы]]
- [[2. В чём разница между intermediate и terminal операциями]]
- [[7. Что делает операция flatMap()]]