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

Що таке операція peek() і коли її використовувати?

Використовується переважно для налагодження:

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

🟢 Junior Level

peek(Consumer) — це проміжна (intermediate) операція, яка виконує дію над кожним елементом, пропускаючи його далі по ланцюжку.

Використовується переважно для налагодження:

// Подивитися елементи між операціями
list.stream()
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("After filter: " + s))
    .map(String::toUpperCase)
    .peek(s -> System.out.println("After map: " + s))
    .collect(Collectors.toList());

У parallelStream() порядок виводу буде непередбачуваним — елементи обробляються різними воркерами ForkJoinPool. У звичайному stream() — порядок збігається з вихідним.

Важливо: peek() нічого не робить без terminal-операції!

🟡 Middle Level

Внутрішня реалізація

public void accept(T t) {
    action.accept(t);       // виконуємо дію
    downstream.accept(t);   // передаємо далі
}

Коли використовувати peek()

  1. Налагодження: Подивитися стан елементів між filter та map
  2. Моніторинг: Заміряти метрики без зміни елементів
  3. Логування: Записати інформацію про підозрілі дані

Не використовуйте peek для production-метрик — у Java 9+ з count()/findFirst() на SIZED джерелах код у peek не виконається. Використовуйте map з логуванням всередині, якщо моніторинг критичний.

Коли НЕ використовувати

  • Для побічних ефектів: Зміна зовнішнього стану робить стрим крихким
  • Замість map: Якщо потрібно змінити об’єкт — використовуйте map
  • Замість forEach: Якщо потрібна дія в кінці — використовуйте forEach

🔴 Senior Level

Пастка оптимізації (Java 9+)

Стрим може взагалі не викликати peek():

long count = Stream.of("A", "B", "C")
    .peek(System.out::println) // Може не виконатися!
    .count();

У Java 9+ метод count() оптимізований: якщо джерело має характеристику SIZED, стрим просто поверне розмір, не проганяючи елементи через конвеєр. SIZED = ArrayList, масив, List.of() — відомий точний розмір. Не-SIZED = Stream.generate(), Iterator, Stream.iterate() — розмір невідомий заздалегідь. Оптимізація з’явилася у Java 9 і збереглася в 11, 17, 21.

Наслідок: Якщо засунули бізнес-логіку в peek — вона буде пропущена.

Production Safety

У продакшн-коді peek повинен зустрічатися вкрай рідко. Якщо бачите його часто — хтось використовує стрими як “наворочені цикли”.

Performance

Кожен peek додає один виклик методу в ланцюжок для кожного елемента. У Highload це зайві такти CPU.

Діагностика

Замість peek(System.out::println) використовуйте вбудований Stream Debugger в IntelliJ IDEA — дозволяє дивитися стан на кожному етапі без модифікації коду.


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

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

  • peek(Consumer) — проміжна операція, виконує дію над елементом і передає його далі
  • Основне призначення — налагодження: перегляд елементів між filter, map, sorted
  • Без terminal-операції peek() нічого не робить — стрим ледачий
  • У Java 9+ peek() може не виконатися на SIZED джерелах з count() — JVM оптимізує пайплайн, пропускаючи весь конвеєр
  • SIZED джерела: ArrayList, масив, List.of(). Не-SIZED: Stream.generate(), Iterator, LinkedList
  • Не використовуйте peek для production-метрик — у деяких випадках код у peek не виконається
  • Кожен peek додає виклик методу на кожен елемент — у Highload це зайві такти CPU

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

  • Чому peek() не виконується у Java 9+ з count()?count() на SIZED джерелі оптимізований: JVM повертає розмір, не проганяючи елементи через конвеєр
  • Чи можна використовувати peek() для бізнес-логіки? — Категорично ні; він може бути пропущений оптимізацією. Використовуйте map() з логуванням всередині
  • Чим peek() відрізняється від forEach()?peek() — проміжна операція (передає елемент далі), forEach() — terminal-операція (кінець пайплайну)
  • У якому порядку peek() виводить елементи у parallelStream? — У непередбачуваному порядку: елементи обробляються різними воркерами ForkJoinPool

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

  • «peek() — гарне місце для production-метрик» — може не виконатися; використовуйте map з логуванням
  • «peek() гарантує виконання на всіх джерелах» — на SIZED джерелах з count() він пропускається
  • «Можна змінювати стан об’єктів у peek()» — це побічний ефект; використовуйте map() для трансформації
  • «peek() і forEach() взаємозамінні» — peek проміжний, forEach terminal; різна семантика

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

  • [[Що таке побічні ефекти (side effects) в Stream]]
  • [[Чому слід уникати побічних ефектів в Stream]]
  • [[Що таке lazy evaluation в Stream]]
  • [[Коли починається виконання операцій в Stream]]