Lazy Evaluation and Short-Circuiting in Streams
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.
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);
}
}filter: Navika
filter: Srihitha
filter: Advitha
map: Advitha
Result: ADVITHAExplanation: 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.
Short-Circuiting Operations
| Operation | Kind | Stops when |
|---|---|---|
limit(n) | Intermediate | n elements have passed |
takeWhile(p) | Intermediate | The first element fails the test |
findFirst(), findAny() | Terminal | One element is found |
anyMatch(p) | Terminal | One element passes |
allMatch(p) | Terminal | One element fails |
noneMatch(p) | Terminal | One element passes |
Example: Infinite Streams Work Because of Laziness
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());
}
}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:
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);
}
}read 85
read 91
read 38
read 78
Lowest: 38Explanation: 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
- Easy: Add
peekto a pipeline offilter,mapandlimit(2)on a list of ten numbers and predict the printed lines before running it. - Harder: Find the first number above 1,000 that is divisible by 7 and 13, using an infinite
Stream.iterateand nolimit.
Key Takeaways
Was this tutorial helpful?