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

Что такое операция peek() и когда её использовать?

Используется в основном для отладки:

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

🟢 Junior Level

peek(Consumer) — это промежуточная операция, которая выполняет действие над каждым элементом, пропуская его дальше по цепочке.

Используется в основном для отладки:

// Посмотреть элементы между операциями
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() — терминальная (конец пайплайна)
  • В каком порядке peek() выводит элементы в parallelStream? — В непредсказуемом порядке: элементы обрабатываются разными воркерами ForkJoinPool

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

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

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

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