Question 19 · Section 8

What is peek() operation and when to use it?

Used mainly for debugging:

Language versions: English Russian Ukrainian

🟢 Junior Level

peek(Consumer) — an intermediate operation that performs an action on each element, passing it further down the chain.

Used mainly for debugging:

// View elements between operations
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());

In parallelStream() the output order will be unpredictable — elements are processed by different ForkJoinPool workers. In a regular stream() — order matches the source.

Important: peek() does nothing without a terminal operation!

🟡 Middle Level

Internal implementation

public void accept(T t) {
    action.accept(t);       // perform the action
    downstream.accept(t);   // pass it further
}

When to use peek()

  1. Debugging: View element state between filter and map
  2. Monitoring: Measure metrics without modifying elements
  3. Logging: Record information about suspicious data

Do not use peek for production metrics — in Java 9+ with count()/findFirst() on SIZED sources, the code in peek will not execute. Use map with logging inside if monitoring is critical.

When NOT to use

  • For side effects: Modifying external state makes the stream fragile
  • Instead of map: If you need to modify an object — use map
  • Instead of forEach: If you need an action at the end — use forEach

🔴 Senior Level

Optimization trap (Java 9+)

The stream may not call peek() at all:

long count = Stream.of("A", "B", "C")
    .peek(System.out::println) // May not execute!
    .count();

In Java 9+, the count() method is optimized: if the source has SIZED characteristic, the stream simply returns the size without passing elements through the pipeline. SIZED = ArrayList, array, List.of() — exact size is known. Non-SIZED = Stream.generate(), Iterator, Stream.iterate() — size is unknown in advance. This optimization appeared in Java 9 and persists in 11, 17, 21.

Consequence: If you put business logic into peek — it will be skipped.

Production Safety

In production code, peek should appear extremely rarely. If you see it often — someone is using streams as “fancy loops”.

Performance

Each peek adds one method call in the chain for every element. In high-load, these are extra CPU cycles.

Diagnostics

Instead of peek(System.out::println) use the built-in Stream Debugger in IntelliJ IDEA — it allows viewing state at each stage without modifying code.


🎯 Interview Cheat Sheet

Must know:

  • peek(Consumer) — intermediate operation, performs an action on the element and passes it further
  • Main purpose — debugging: viewing elements between filter, map, sorted
  • Without a terminal operation peek() does nothing — the stream is lazy
  • In Java 9+ peek() may not execute on SIZED sources with count() — JVM optimizes the pipeline, skipping the entire chain
  • SIZED sources: ArrayList, array, List.of(). Non-SIZED: Stream.generate(), Iterator, LinkedList
  • Do not use peek for production metrics — in some cases the code in peek will not execute
  • Each peek adds a method call per element — in high-load, these are extra CPU cycles

Frequent follow-up questions:

  • Why doesn’t peek() execute in Java 9+ with count()?count() on a SIZED source is optimized: JVM returns the size without passing elements through the pipeline.
  • Can I use peek() for business logic? — Absolutely not; it may be skipped by optimization. Use map() with logging inside.
  • How does peek() differ from forEach()?peek() is an intermediate operation (passes the element further), forEach() is terminal (end of the pipeline).
  • In what order does peek() output elements in parallelStream? — In unpredictable order: elements are processed by different ForkJoinPool workers.

Red flags (DO NOT say):

  • “peek() is a good place for production metrics” — it may not execute; use map with logging
  • “peek() guarantees execution on all sources” — on SIZED sources with count() it is skipped
  • “You can change object state in peek()” — that’s a side effect; use map() for transformation
  • “peek() and forEach() are interchangeable” — peek is intermediate, forEach is terminal; different semantics

Related topics:

  • [[What are side effects in Stream]]
  • [[Why you should avoid side effects in Stream]]
  • [[What is lazy evaluation in Stream]]
  • [[When does Stream operation execution begin]]