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

Что такое Collector и какие есть встроенные Collectors?

Пример использования:

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

🟢 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>

  1. supplier() — создает аккумулятор (ArrayList::new)
  2. accumulator() — добавляет элемент (List::add)
  3. combiner() — объединяет аккумуляторы (для parallelStream)
  4. finisher() — финальное преобразование
  5. 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

  1. Простое накопление в List/Set — используйте toList(), toSet() (Java 16+: toList() immutable)
  2. Группировка с одним уровнемgroupingBy() покрывает 95% случаев
  3. Ваш 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()]]