Laziness in Java
Nothing runs until a terminal operation; element-by-element processing.
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.
Silent filter
What does this print?
Stream.of(1, 2, 3)
.filter(n -> {
System.out.print(n);
return true;
});
System.out.println("end");123endendend123
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.
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 + " "));f1 f2 e1 e2f1 e1 f2 e2e1 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.
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()); // 0Lazy 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
- No terminal operation → nothing runs at all
- Elements flow one at a time through every stage
- Laziness lets findFirst/limit stop early and skip work
- Infinite sources are possible because of laziness
💡 A lazy stream is a recipe card: nothing gets cooked until someone actually orders the dish.
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");- abdone
- done
- doneab
- 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?
- Once per item: one million times
- Twice
- Once
- 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.