Питання 5 · Розділ 8

Що робить операція collect()?

Найчастіший випадок — збирання в List:

Мовні версії: English Russian Ukrainian

🟢 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 функцій:

  1. supplier() — створює новий контейнер (ArrayList::new)
  2. accumulator() — додає елемент у контейнер (List::add)
  3. combiner() — об’єднує два контейнери (потрібно для parallelStream)
  4. 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

  1. Простий підрахунок — використовуйте count() замість collect(toList()).size()
  2. Перевірка існуванняanyMatch() замість collect(toList()) і перевірки isEmpty()
  3. Пошук одного елемента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()]]