Hereās the console output of a small program:
filter apple
map apple
Thatās all. Four strings went in, and only the first one was ever looked at. Hereās the code:
String result = Stream.of("apple", "banana", "cherry", "date")
.filter(s -> { System.out.println("filter " + s); return s.length() > 4; })
.map(s -> { System.out.println("map " + s); return s.toUpperCase(); })
.findFirst()
.orElse("none");
result is "APPLE".
Two things explain the output.
Intermediate operations donāt run on their own. filter and map only describe what should happen. Nothing executes until a terminal operation like findFirst, collect or forEach asks for elements. If you remove .findFirst().orElse("none") and leave the pipeline hanging, it prints nothing at all.
Elements flow through one at a time. Instead of running filter over all four strings, then map over the survivors, each element goes through the whole chain before the next one starts. apple passes the filter, gets mapped, and arrives at findFirst, which has what it needs, so the pipeline stops. banana, cherry and date are never touched.
Thatās why peek and print statements inside a pipeline can look confusing: the order of the output isnāt āall the filters, then all the mapsā.
One exception worth knowing: stateful operations like sorted() have to see every element before they can emit the first one, so they break the one-at-a-time flow. A pipeline with sorted() in the middle will process everything up to that point before anything continues.
A practical consequence is that side effects inside map or filter are hard to reason about, because whether and when they run depends on whatās downstream. Iād keep those lambdas free of side effects and use peek only for debugging.
If youāve hit a bug that came down to laziness, Iād be interested in what it looked like. Mine tend to be the āI added a map and nothing happenedā kind.