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

В чому різниця між reduce() та collect()?

Обидві операції згортають стрим у результат, але роблять це по-різному:

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

🟢 Junior Level

Обидві операції згортають стрим у результат, але роблять це по-різному:

reduce() — створює новий об’єкт на кожному кроці:

// Підсумовування — кожен крок створює нове число
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

collect() — змінює один і той самий об’єкт:

// Додавання в один і той самий список
List<String> list = strings.stream().collect(Collectors.toList());

Просте правило:

  • Числа, рядки → reduce()
  • Колекції (List, Set, Map) → collect()

🟡 Middle Level

Тип редукції

reduce() — Immutable Reduction:

  • На кожному кроці створюється новий об’єкт
  • Приклад: (String a, String b) -> a + b створює новий рядок, копіюючи вміст обох

collect() — Mutable Reduction:

  • На кожному кроці модифікується існуючий контейнер
  • Приклад: StringBuilder.append() доповнює вже виділений буфер

Складність алгоритму

При збірці в колекцію:

  • reduce: O(n²) для конкатенації рядків. Кожна конкатенація a + b копіює всі символи обох рядків. Для n рядків довжиною 1: крок 1 копіює 1 символ, крок 2 — 2… Разом 1+2+3+…+n = n(n+1)/2 = O(n²). reduce технічно може зібрати List: reduce(new ArrayList<>(), (list, el) -> { list.add(el); return list; }, ...). Але це зламає паралельний режим: мутабельний identity використовується у всіх гілках одночасно. Тому — тільки collect (створює окремий контейнер для кожної гілки).
  • collect: O(n) — просто додає посилання в ArrayList

Decision Matrix

Задача Операція
Числа, примітиви reduce() або sum(), count()
String конкатенація collect(joining())
List, Set, Map Тільки collect()
Складні DTO collect()

🔴 Senior Level

Паралелізм та Combiner

Характеристика reduce() collect()
Пам’ять Висока витрата (проміжні об’єкти) Низька (контейнери перевикористовуються)
Combiner Об’єднує два значення Об’єднує два контейнери
Оптимізація Складно через алокації Characteristics.CONCURRENT дозволяє писати в один контейнер

Identity Value відмінність

  • У reduce: Початкове значення використовується багаторазово в паралельних гілках
  • У collect: Для кожної гілки створюється свій контейнер через supplier()

Side Effects

  • У collect() мутація контейнера — основа роботи
  • У reduce() мутація вхідних параметрів акумулятора — груба помилка, зламає логіку в паралельному режимі

Діагностика

Profiling: Якщо бачите величезну алокацію char[] при роботі з рядками — використовуєте reduce там, де потрібен collect(joining()).

Unit Testing: Завжди запускайте кастомні колектори на parallelStream() з малим батчем для перевірки combiner.


🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • reduce() — immutable reduction: створює новий об’єкт на кожному кроці. collect() — mutable reduction: модифікує один контейнер
  • Алгоритмічна складність: reduce для рядків O(n²) (копіювання символів), collect O(n) (додавання посилання)
  • Золоте правило: числа/рядки → reduce(), колекції (List, Set, Map) → collect()
  • У reduce identity використовується багаторазово в паралельних гілках; у collect для кожної гілки створюється свій контейнер через supplier()
  • collect підтримує Characteristics.CONCURRENT — кілька потоків пишуть в один контейнер, reduce — ніколи
  • Мутація вхідних параметрів акумулятора у reduce — груба помилка, зламає паралельний режим
  • Decision Matrix: примітиви → reduce/sum/count, String → collect(joining()), List/Set/Map → тільки collect()

Часті уточнюючі запитання:

  • Чому reduce для конкатенації рядків — O(n²)? — Кожна конкатенація a + b копіює всі символи обох рядків; сума 1+2+3+…+n = n(n+1)/2
  • Чи може reduce зібрати List? — Технічно так, але це зламає паралельний режим (мутабельний identity у гілках). Використовуйте collect
  • У чому різниця identity у reduce vs supplier у collect? — identity у reduce — один об’єкт, що перевикористовується у всіх гілках. supplier у collect — створює окремий контейнер для кожної гілки
  • Як діагностувати misuse reduce/collect? — Профілювання: величезна алокація char[] при роботі з рядками = потрібен collect(joining()) замість reduce

Червоні прапорці (НЕ говорити):

  • «reduce і collect — одне й те саме, просто різні імена» — фундаментально різний підхід: immutable vs mutable reduction
  • «Можна мутувати identity у reduce для оптимізації» — це зламає паралельний режим і дасть неправильний результат
  • «collect не підтримує паралелізм» — підтримує через combiner та CONCURRENT characteristic
  • «reduce завжди повільніший за collect» — для чисел та іммутабельних об’єктів різниця непомітна; collect швидше для колекцій

Пов’язані теми:

  • [[Що робить операція reduce()]]
  • [[Що робить операція collect()]]
  • [[Що таке Collector та які є вбудовані Collectors]]
  • [[Які потенційні проблеми можуть бути з паралельними стримами]]