Java 24: Stream API — Lazy Evaluation
easy⏱ 5 mincoursejava
Intermediate vs terminal
Intermediate ops return a Stream — filter, map, flatMap, distinct, sorted, peek, limit, skip. They're lazy — just recorded. Terminal ops consume the stream and return a result (or void) — forEach, count, collect, reduce, findFirst, anyMatch. Only a terminal op kicks off execution.
IntStream.range(0, 1_000_000)
.map(x -> x * 2) // lazy
.filter(x -> x > 10) // lazy
.findFirst(); // terminal — triggers execution, stops at first match
Element-at-a-time
Streams pipe one element through all stages before moving to the next — not stage-by-stage. stream.filter(...).map(...).findFirst() pulls the first source element through filter, then map, then yields it — if it passes. findFirst can short-circuit the entire pipeline after one match.
Streams are single-use
Once a terminal op runs, the stream is consumed — reusing it throws IllegalStateException. Build a fresh stream from the source. Think of them as iterators, not collections.
Build a traced stream
Implement TracedStream<T> with filter, map, forEach, findFirst. Log every element + stage visited. Process [1..1000], filter even, map *3, findFirst where > 50. You should stop after just 6 elements — proof of short-circuit.
peek for debugging, not side effects
.peek(System.out::println) is designed for debugging lazy pipelines — it runs only when elements flow through. Never rely on peek for real side effects — if the terminal op short-circuits, not every element gets peeked.
Quiz: sorted + findFirst
stream.sorted().findFirst() — does this visit every element? Yes — sorted is a stateful intermediate that must buffer everything before emitting the first result. Short-circuit can't kick in. If you only need the minimum, use min(Comparator) instead.