What does lazy evaluation mean in a Java stream, and how can you actually see it happening?
Lazy evaluation means the intermediate operations in a pipeline do no work when you call them. Calling filter or map on a stream does not touch a single element. Each of those calls returns a new stream that records what should happen, and the elements are only pulled through the whole chain when a terminal operation is called.
You can see it with peek, which is a debug-only operation that runs its action for each element as it passes through. Put a peek before and after other stages and you will see the order the pipeline actually processes things in — one element at a time, all the way through the chain, before the next one starts.
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// Nothing runs until a terminal operation.
System.out.println("created but not consumed");
List<Integer> doubled = numbers.stream()
.peek(n -> System.out.println(" peek: " + n))
.map(n -> {
System.out.println(" map: " + n);
return n * 2;
})
.filter(n -> {
System.out.println(" filter: " + n);
return n > 4;
})
.collect(Collectors.collect(java.util.stream.Collectors.toList()));
System.out.println("result : " + doubled);
}
}The output is the clearest possible demonstration. Nothing happened while the pipeline was being built. Then, once collect was called, the elements flowed through one at a time: the first element was peeked, mapped and filtered before the second element was even peeked.
That per-element flow is what makes laziness useful. It means an infinite stream is possible — Stream.iterate(1, i -> i + 1) never ends, but limit(10) stops it. It means short-circuiting operations work: findFirst stops pulling elements the moment it has an answer, so searching a million-element list for one match does not read a million elements. And it means a pipeline over an expensive source only reads as much as it needs.
One caveat about peek. It is intended for debugging, and its position in the pipeline is not fully specified — the order in which multiple peeks fire relative to each other is not guaranteed. Do not build logic on top of it.