What is peek() operation and when to use it?
Used mainly for debugging:
🟢 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 differentForkJoinPoolworkers. In a regularstream()— 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()
- Debugging: View element state between
filterandmap - Monitoring: Measure metrics without modifying elements
- Logging: Record information about suspicious data
Do not use
peekfor production metrics — in Java 9+ withcount()/findFirst()onSIZEDsources, the code inpeekwill not execute. Usemapwith 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 onSIZEDsources withcount()— JVM optimizes the pipeline, skipping the entire chain - SIZED sources:
ArrayList, array,List.of(). Non-SIZED:Stream.generate(),Iterator,LinkedList - Do not use
peekfor production metrics — in some cases the code inpeekwill not execute - Each
peekadds 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]]