Lazy Evaluation and Short-Circuiting in Streams

Naveen AnamaneniUpdated · 5 min read

Lazy evaluation means a stream does no work until it must. Intermediate operations only describe the steps; the terminal operation then pulls elements through the pipeline one at a time, each element passing through every step before the next one starts. Short-circuiting means some operations, such as limit, findFirst and anyMatch, can stop early once the answer is known. Together, these features make streams efficient and let them work with infinite sources. In this lesson, you will see the real order in which a stream does its work.

Example: Elements Flow One at a Time

Let's see an example that prints each step as it happens.

Vertical.java▶ Try it
import java.util.List;
 
public class Vertical {
    public static void main(String[] args) {
        List<String> names =
            List.of("Navika", "Srihitha", "Advitha", "Anamaneni");
 
        String first = names.stream()
            .filter(n -> {
                System.out.println("filter: " + n);
                return n.startsWith("A");
            })
            .map(n -> {
                System.out.println("map:    " + n);
                return n.toUpperCase();
            })
            .findFirst()
            .orElse("none");
 
        System.out.println("Result: " + first);
    }
}
Output
filter: Navika
filter: Srihitha
filter: Advitha
map:    Advitha
Result: ADVITHA

Explanation: The stream did not filter all four names first and then map them. It took Navika through filter (rejected), then Srihitha (rejected), then Advitha (accepted), which went straight on to map. At that moment findFirst had its answer, so Anamaneni was never looked at, not even by filter.

Each element travels the whole pipeline before the next one starts

Short-Circuiting Operations

OperationKindStops when
limit(n)Intermediaten elements have passed
takeWhile(p)IntermediateThe first element fails the test
findFirst(), findAny()TerminalOne element is found
anyMatch(p)TerminalOne element passes
allMatch(p)TerminalOne element fails
noneMatch(p)TerminalOne element passes

Example: Infinite Streams Work Because of Laziness

Infinite.java▶ Try it
import java.util.stream.Stream;
 
public class Infinite {
    public static void main(String[] args) {
        int firstBig = Stream.iterate(1, n -> n * 3)
            .filter(n -> n > 500)
            .findFirst()
            .get();
        System.out.println("First power of 3 above 500: " + firstBig);
 
        System.out.println(Stream.iterate(1, n -> n + 1)
            .map(n -> n * n)
            .limit(5)
            .toList());
    }
}
Output
First power of 3 above 500: 729
[1, 4, 9, 16, 25]

Explanation: Stream.iterate(1, n -> n * 3) has no end. Because the stream is lazy, it creates only as many values as needed: findFirst stops at 729, and limit(5) stops after five squares.

Operations That Must See Everything

Some intermediate operations can't produce any output until they have seen every element. They are called stateful: sorted() and, to a lesser extent, distinct(). Putting sorted() before findFirst() forces the stream to read the whole source:

Stateful.java▶ Try it
import java.util.List;
 
public class Stateful {
    public static void main(String[] args) {
        List<Integer> marks = List.of(85, 91, 38, 78);
        int lowest = marks.stream()
            .peek(m -> System.out.println("read " + m))
            .sorted()
            .findFirst()
            .get();
        System.out.println("Lowest: " + lowest);
    }
}
Output
read 85
read 91
read 38
read 78
Lowest: 38

Explanation: sorted() had to read all four marks before it knew which one came first. This is why sorted() on an infinite stream never finishes.

Common Mistakes

Using sorted() or distinct() on an infinite stream. It never ends.

Relying on side effects inside lambdas to happen for every element. With short-circuiting, some elements are never processed.

Putting limit after an expensive map. limit first is usually cheaper, if the meaning allows it.

Interview Corner

What does lazy evaluation mean in Java streams?

Intermediate operations don't process anything when they are called; they only build a description of the pipeline. Processing starts when a terminal operation is invoked, and elements are then pulled through the pipeline one at a time, each passing through all stages before the next. This allows short-circuiting operations such as limit, findFirst and anyMatch to stop early, avoids unnecessary work, and makes infinite streams usable. Stateful operations like sorted are the exception: they must consume all input first.

Practice

  1. Easy: Add peek to a pipeline of filter, map and limit(2) on a list of ten numbers and predict the printed lines before running it.
  2. Harder: Find the first number above 1,000 that is divisible by 7 and 13, using an infinite Stream.iterate and no limit.

Quick check: Lazy evaluation

4 questions
1When do intermediate operations run?
2How do elements move through a pipeline?
3Why can't sorted() finish on an infinite stream?
4Which operation can stop a pipeline early?

Key Takeaways

Was this tutorial helpful?

Lesson 316 of 482