GoByte Skills Episode 6 of 27, track Java (3 of 3)

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.

Back to top

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.

Animation for GoByte Skills #6: Elements flow through the pipeline one at a time, only once the terminal call arrives.
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.

Report a mistake

Your product here? Partner with us

Back to top

Discussion

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.