Питання 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()]]