Вопрос 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]]
  • [[Какие потенциальные проблемы могут быть с параллельными стримами]]