Skip to content
Theory · Lambdas and the Stream API · lesson 9 of 12

Lazy streams

In the previous lesson the belt ended with the toList() collector. Let's remove the collector and see what the belt does. The card for the filter station will print each word before checking it:

JAVA
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("cat", "ox");
        words.stream().filter(w -> {
            System.out.println("check " + w);
            return w.length() > 2;
        });
        System.out.println("finished"); // finished
    }
}

The program prints only finished. The lines check cat and check ox are missing: the lambda in filter never ran.

Without a collector the belt stands still

This is the main difference between a stream and a loop. A for loop runs its body right away. But filter and map do nothing with the elements: they only hand the stations their cards and return a new stream.

The belt starts moving only when a collector, a terminal operation, takes its place at the end. Operations like filter and map are called lazy: the work is put off until the result is needed. No terminal operation, no work.

What runs in a stream without a terminal operation is a common interview question. The answer: nothing.

Elements ride one at a time

Fine, the collector is in place. In what order do the stations work then? You might think filter checks all the elements first and then map handles everyone who passed. In fact, each element rides the whole belt, and only then does the next one get on.

The widget has two cards: isLong for the filter station and up for the map station, and each prints what it does. The frame isLong.test is the body of the isLong lambda, which the stream calls through the test method, and the frame up.apply is the body of the up lambda. Between Main.main and these frames, the stream's own methods are actually running, but the widget does not show them. Press "Next step" and watch the order of the lines in the output.

LiveElements ride one at a timestep 1 of 12 · frames: 1

isLong is the card for the filter station: the lambda's body does not run now.

The top frame is the lambda running now

Look at the output. built printed before everything else, even though filter and map stand higher up in the program. cat rode through both stations before ox got on the belt, and ox never reached map at all.

This order gives a stream two benefits. It needs no intermediate lists: each element goes straight on. And an operation that needs only the first matching element can stop the belt and leave the rest untouched.

This is where the belt picture is inaccurate. A real belt carries many boxes at once, one behind another. A stream's belt holds only one element at any moment: the next one gets on when the previous one has reached the end or left the belt.

Common mistake

A stream was stored in a variable and run twice:

JAVA
var s = words.stream().filter(w -> w.length() > 2);
System.out.println(s.count()); // 1
System.out.println(s.toList()); // ✗
TEXT
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed

The first print outputs 1, and the program fails on the second terminal operation. A stream is single-use: after the collector the belt is taken apart, and it cannot run a second time. If you need another result, build a new stream: you can call words.stream() as many times as you like.

Key points

  • Intermediate operations only build the belt: without a terminal operation, no lambda runs.
  • Each element passes all the stations in a row, and only then does the stream take the next one.
  • An element that did not pass filter never reaches the following stations.
  • A stream is single-use: a second terminal operation throws IllegalStateException.