Вопрос 17 · Раздел 8

Что делает операция reduce()?

Пошаговый разбор для [1, 2, 3, 4, 5]:

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

reduce() — это терминальная операция, которая сворачивает стрим в одно значение.

Три варианта:

// 1. Без начального значения — возвращает Optional
Optional<Integer> sum = numbers.stream()
    .reduce((a, b) -> a + b);

// 2. С начальным значением
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b);

// 3. С изменением типа
int lengthSum = strings.stream()
    .reduce(0, (total, str) -> total + str.length(), Integer::sum);

Простой пример:

List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// Результат: 15

Пошаговый разбор для [1, 2, 3, 4, 5]:

  • Шаг 1: a=0 (identity), b=1 → результат 1
  • Шаг 2: a=1 (накопленное), b=2 → результат 3
  • Шаг 3: a=3, b=3 → результат 6
  • Шаг 4: a=6, b=4 → результат 10
  • Шаг 5: a=10, b=5 → результат 15

🟡 Middle Level

Математические требования

Для корректной работы reduce (особенно в параллельном режиме):

  • Identity: identity — «нейтральный элемент»: accumulator.apply(identity, x) = x. Для сложения identity = 0 (0 + x = x). Для умножения identity = 1 (1 × x = x). Если взять identity = 1 для сложения — результат будет завышен на количество элементов!
  • Associativity: (a op b) op c == a op (b op c)
  • Commutativity: Для параллельной обработки желательно, чтобы порядок не влиял на результат

Reduce vs Collect

Это “золотой вопрос” интервью:

  • reduce: Создает новый объект на каждом шаге. Хорош для чисел, строк, иммутабельных объектов
  • collect: Модифицирует существующий контейнер. Эффективнее для коллекций
// ПЛОХО — создает миллионы строк O(n²)
stream.reduce("", String::concat)

// ХОРОШО — использует один StringBuilder O(n)
stream.collect(Collectors.joining())

Боксинг

При использовании reduce на Integer/Long — автобоксинг. Для Highload предпочитайте примитивные стримы: IntStream.sum(), LongStream.max().

🔴 Senior Level

Третья сигнатура reduce

<U> U reduce(U identity, 
             BiFunction<U, ? super T, U> accumulator, 
             BinaryOperator<U> combiner)

Combiner критически важен для parallelStream() — учит стрим объединять результаты из разных потоков.

Identity Mutation

Никогда не меняйте объект identity. Он используется многократно в параллельных ветках. Если измените — аффектите все параллельные вычисления.

Edge Cases

  • Parallel Combiner: Несовместимый комбинер → неверный результат без исключений
  • Null Handling: Аккумулятор, возвращающий null → NPE
  • Empty Streams: Версия с одним аргументом возвращает Optional — не делайте .get() без проверки

Диагностика

Используйте IntelliJ Stream Debugger — визуально показывает, как accumulator объединяет элементы на каждом шаге.


🎯 Шпаргалка для интервью

Обязательно знать:

  • reduce() — терминальная операция, сворачивающая стрим в одно значение
  • Три сигнатуры: (1) с BinaryOperatorOptional, (2) с identity + BinaryOperator, (3) с identity + accumulator + combiner (для параллелизма и смены типа)
  • Математические требования: Identity (identity op x = x), Associativity ((a op b) op c = a op (b op c)), желательна Commutativity
  • reduce() создаёт новый объект на каждом шаге (immutable reduction); для коллекций используйте collect()
  • Для конкатенации строк: reduce("", String::concat) — O(n²), collect(Collectors.joining()) — O(n)
  • Identity mutation — грубая ошибка: объект identity используется многократно в параллельных ветках
  • На parallelStream() обязателен корректный combiner — иначе неверный результат без исключений

Частые уточняющие вопросы:

  • Почему reduce с identity = 1 для сложения даст неверный результат? — identity должен быть нейтральным элементом: 0 для сложения, 1 для умножения. Иначе результат завышен на число элементов
  • Чем reduce отличается от collect? — reduce создаёт новый объект на каждом шаге (immutable), collect модифицирует один контейнер (mutable). reduce для чисел/строк, collect для коллекций
  • Зачем третий параметр combiner в reduce? — Объединяет результаты из разных потоков в parallelStream(); без него параллельный режим некорректен
  • Что вернёт reduce на пустом стриме? — Версия без identity → Optional.empty(). Версия с identity → значение identity

Красные флаги (НЕ говорить):

  • «reduce можно использовать для сборки List» — технически можно, но сломает параллельный режим (мутабельный identity в ветках)
  • «Порядок в reduce всегда гарантирован» — в параллельном режиме порядок зависит от combiner
  • «Identity можно менять внутри аккумулятора» — это сломает все параллельные вычисления
  • «reduce и collect взаимозаменяемы» — collect эффективнее для коллекций O(n) vs O(n²) у reduce

Связанные темы:

  • [[В чём разница между reduce() и collect()]]
  • [[Что делает операция collect()]]
  • [[Какие потенциальные проблемы могут быть с параллельными стримами]]
  • [[Можно ли изменять состояние внешних переменных в Stream операциях]]