Streams: nothing runs until the end
GoByte Skills #6: filter and map do not filter or map anything. They write down a recipe, and no element moves until a terminal call drives the pipeline, pushing elements through one at a time. Bonus: why count() can skip your peek.
Piere is one of GoByte's characters. This post was drafted by AI agents in Piere's voice, then fact checked, run and edited by the GoByte team.
A stream is a recipe, and no element is touched until a terminal operation asks for a result. filter and map do not filter or map anything. They return a new Stream that remembers one more stage.
Transcript
The Java snippet types in while the pipeline stages appear empty; only when findFirst arrives does a push start at the source, and numbered elements flow through filter and map one at a time while 3 and 4 never leave the source.
import java.util.stream.Stream;
class Main {
static void say(Object o) { System.out.println(o); }
public static void main(String[] args) {
var s = Stream.of(1, 2, 3, 4)
.filter(n -> { say("filter " + n);
return n % 2 == 0; })
.map(n -> { say("map " + n);
return n * 10; });
say("built");
say(s.findFirst().orElseThrow());
}
}
It prints:
built
filter 1
filter 2
map 2
20
Why#
Each intermediate call only links a stage onto a pipeline object. The terminal call (findFirst, collect, count, forEach) drives the pipeline: it wires the stages into a chain of sinks, then pushes the source through that chain one element at a time. The order is vertical: 2 is filtered and mapped before 3 is read. Because findFirst short circuits, 3 and 4 are never touched.
Laziness is also why an infinite source such as Stream.iterate(1, n -> n + 1) is fine: findFirst stops it at the first match.
Where it breaks#
- Stateful stages are barriers.
sorted()must see every element before it emits the first, so laziness stops there, and an infinite stream never gets past it. - A stream is single use. A second terminal call throws
IllegalStateException: stream has already been operated upon or closed. - Since Java 9,
count()may skip the pipeline when the source knows its size.Stream.of(1, 2, 3).peek(System.out::println).count()prints nothing and returns 3. Side effects in intermediate stages are not a contract.
Rule of thumb#
Keep intermediate lambdas pure, use peek only to debug, and put the work in the terminal call. A stream with no terminal operation is a meeting with no action items: it compiles, it looks busy, and nothing happens.
Your product here? Partner with us
Back to topDiscussion
No comments yet. Signed in GoByte members with a verified e-mail can join. Community guidelines
Reading is open to everyone. Commenting and voting need a GoByte account with a verified e-mail.