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

Що таке побічні ефекти (side effects) в Stream?

Побічні ефекти роблять код непередбачуваним, особливо в паралельних стримах.

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

🟢 Junior Level

Побічний ефект (side effect) — це будь-яка дія всередині стрима, яка змінює щось за межами самого стрима.

Приклади побічних ефектів:

  • Зміна зовнішніх змінних
  • Друк у консоль (System.out.println)
  • Запис у файл або базу даних
  • Зміна об’єктів всередині стрима
// Побічний ефект — зміна зовнішньої змінної
List<String> result = new ArrayList<>();
stream.forEach(result::add);

// Побічний ефект — вивід у консоль
stream.peek(System.out::println).collect(toList());

Побічні ефекти роблять код непередбачуваним, особливо в паралельних стримах.

🟡 Middle Level

Що вважається побічним ефектом

  1. Зміна значень зовнішніх змінних (мутація об’єктів у купі)
  2. Виконання I/O операцій (друк, запис у файл, мережевий запит)
  3. Зміна стану елементів всередині стрима

Три принципи безпечного стрима

Для коректної роботи (особливо паралельної) функції повинні бути:

  1. Identity: Початкове значення не повинно змінювати результат
  2. Associativity: Порядок групування не впливає на результат
  3. Non-interference: Джерело даних не повинно змінюватися під час виконання стрима

Небезпеки

Непередбачуваність: У паралельному стримі побічні ефекти виконуються у випадковому порядку.

Зниження продуктивності: Побічні ефекти вимагають синхронізації — потоки вишиковуються в чергу.

Легітимні побічні ефекти

Тільки в двох місцях:

  • forEach(): Terminal-операція для дій “у зовнішній світ”
  • peek(): Тільки для налагодження (логування проміжних станів)

🔴 Senior Level

The Atomic Reference Trap

AtomicInteger для підрахунку всередині forEach паралельного стрима:

  • 100 потоків пишуть в одну кеш-лінію (cache line — блок пам’яті 64 байти, який CPU завантажує цілком). Два потоки, що пишуть у сусідні байти, змушують CPU постійно перечитувати одну й ту саму лінію (cache thrashing).
  • Стрим повільніший за звичайний цикл for

Transactional Side Effects

У більшості випадків не робіть repo.save() всередині map або forEach паралельного стрима. Виняток: batch-обробка, де кожен запис у БД незалежний і ви контролюєте транзакції вручну.

Logging Overhead

stream.peek(log::info) у Highload може сповільнити обробку в 100 разів через блокування всередині логера.

Edge Cases

Missing Side Effects: Якщо використовуєте peek() для бізнес-логіки — у Java 9+ при terminal-операції count() на джерелах з характеристикою SIZED (ArrayList, масив) — JVM оптимізує count() і пропускає весь пайплайн. На не-SIZED джерелах (LinkedList, Stream.generate()) — peek() виконається.

Deadlock Potential: Побічний ефект, що захоплює блокування, може призвести до дедлоку з іншим потоком ForkJoinPool.

Діагностика

Завжди тестуйте стрими з побічними ефектами на parallelStream(). Якщо результати “плавають” — баг у дизайні.


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

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

  • Побічний ефект — будь-яка дія всередині стрима, що змінює щось за його межами (мутація змінних, I/O, друк)
  • Побічні ефекти роблять паралельні стрими непередбачуваними: порядок виконання випадковий
  • Три принципи безпечного стрима: Identity (нейтральний елемент), Associativity (порядок групування не важливий), Non-interference (джерело не змінюється)
  • Легітимні побічні ефекти тільки в двох місцях: forEach() (terminal) і peek() (налагодження)
  • AtomicInteger у forEach паралельного стрима: cache line contention — стрим повільніший за звичайний цикл for
  • У Java 9+ peek() може не виконатися на SIZED джерелах з count() — JVM оптимізує пайплайн
  • stream.peek(log::info) у Highload сповільнює обробку до 100x через блокування логера

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

  • Що вважається побічним ефектом? — Мутація зовнішніх змінних, I/O, зміна стану об’єктів всередині стрима
  • Чи можна використовувати peek() для бізнес-логіки? — Категорично ні; у Java 9+ він може не виконатися на SIZED джерелах з count()
  • Що таке Non-interference? — Джерело даних стрима не повинно змінюватися під час виконання пайплайна, інакше ConcurrentModificationException
  • Коли side effect допустимий у forEach? — Коли дія необоротна та одноразова: відправка email, запис у лог

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

  • «peek() — нормальне місце для бізнес-логіки» — він може не виконатися; для production використовуйте map()
  • «AtomicInteger — рішення всіх проблем паралелізму» — contention на CAS вбиває продуктивність
  • «Побічні ефекти безпечні у звичайному стримі» — вони блокують майбутній паралелізм і порушують функціональну парадигму
  • «repo.save() всередині map паралельного стрима — це нормально» — некеровані транзакції, немає rollback, вичерпання пулу з’єднань

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

  • [[Чому слід уникати побічних ефектів в Stream]]
  • [[Чи можна змінювати стан зовнішніх змінних в Stream операціях]]
  • [[Що таке операція peek() і коли її використовувати]]
  • [[Які потенційні проблеми можуть бути з паралельними стримами]]