Що таке 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()]]