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

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

Покроковий розбір для [1, 2, 3, 4, 5]:

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

🟢 Junior Level

reduce() — це terminal-операція, яка згортає стрим в одне значення.

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

// 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() — terminal-операція, що згортає стрим в одне значення
  • Три сигнатури: (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 операціях]]