🌊 Streams API · Intermediate

Laziness in Java

Nothing runs until a terminal operation; element-by-element processing.

🧩 The mysteryYou put a print statement inside filter, run the program… and nothing prints. No error, no output. Is the stream broken, or just lazy?

The belt starts only when someone orders

Think of a factory belt that stays still until a customer places an order at the end. Intermediate ops like filter and map just write down the recipe. Nothing runs until a terminal operation asks for results.

🔮 Predict it

Silent filter

What does this print?

Stream.of(1, 2, 3)
    .filter(n -> {
        System.out.print(n);
        return true;
    });
System.out.println("end");
  1. 123end
  2. end
  3. end123
Show the answer

Only end. There's no terminal operation, so the pipeline never executes and the predicate never runs.

One element at a time

When a terminal op runs, it pulls elements one by one. Each element travels through the whole pipeline before the next one starts — not stage by stage over the whole list.

🔮 Predict it

Who goes first?

What does this print?

Stream.of(1, 2)
    .filter(n -> {
        System.out.print("f" + n + " ");
        return true;
    })
    .forEach(n ->
        System.out.print("e" + n + " "));
  1. f1 f2 e1 e2
  2. f1 e1 f2 e2
  3. e1 e2 f1 f2
Show the answer

f1 e1 f2 e2 — element 1 goes all the way through filter and forEach before element 2 even starts. The stages interleave.

Laziness saves work

Because elements flow one at a time, a short-circuiting terminal op can stop early. list.stream().filter(this::isValid).findFirst() on a million items, where item #2 is valid, calls isValid only twice. Laziness is also what makes infinite streams possible.

⚠️ The trap

Forgetting the terminal op

peek is intermediate, so this pipeline never runs and out stays empty: it prints 0. Add a terminal op — or better, let the stream build the list with toList().

List<String> out = new ArrayList<>();
Stream.of("x", "y", "z")
    .map(String::toUpperCase)
    .peek(out::add);      // never runs!
System.out.println(out.size()); // 0
💼 In the real world

Lazy in production

Laziness is why findFirst on a huge database result or log file can be fast: work stops at the first match. It's also behind a classic code-review bug — a pipeline built for its side effects, with no terminal op, that silently does nothing.

Key takeaways

  1. No terminal operation → nothing runs at all
  2. Elements flow one at a time through every stage
  3. Laziness lets findFirst/limit stop early and skip work
  4. Infinite sources are possible because of laziness

💡 A lazy stream is a recipe card: nothing gets cooked until someone actually orders the dish.

🤯 Did you know?

Laziness is why creating the infinite stream Stream.iterate(1, n -> n + 1) returns instantly: building a pipeline computes nothing at all.

Practice questions

What does this print?

Stream<String> s = Stream.of("a", "b")
    .map(x -> {
        System.out.print(x);
        return x;
    });
System.out.println("done");
  1. abdone
  2. done
  3. doneab
  4. ab
Check your answer

done. Without a terminal operation the pipeline never executes, so the print inside map never runs. Only "done" is printed.

list.stream().filter(this::isValid).findFirst() runs on a list of one million items, and item #2 is valid. How many times does isValid run?

  1. Once per item: one million times
  2. Twice
  3. Once
  4. It depends on the list's hash codes
Check your answer

Twice. Because streams are lazy and process elements one by one, findFirst stops pulling as soon as item #2 passes. Only items #1 and #2 are tested.

Next: the two workhorses of every pipeline — filter keeps, map transforms. But which one keeps what?