The labs on this site need JavaScript to run. The same content is written out below.
Learn › Java › Lambdas and streams
Lazy evaluation in streams
A stream does not run in the order in which it is written
Intermediate operations such as filter and map do nothing at the moment they are called. They run only when the terminal operation is called. Even then, the stream does not finish one operation for all the elements before it starts the next. It sends the elements through to the end one at a time.
Checked against real execution · Checked by running 12 combinations on JDK 21
Concepts in this lab
- Stream API
- This API expresses filtering, transformation, and aggregation over data such as collections in a declarative way. You chain intermediate operations such as filter and map, and finish with a terminal operation such as collect or forEach. Intermediate operations do not run until the terminal operation is called (lazy evaluation), and a stream does not modify its source data and can be used only once.
- Lazy evaluation
- This approach delays a computation until its result is actually needed. The intermediate operations of a stream do not run as soon as they are called. They run only when the terminal operation is called, and each element passes through the pipeline one at a time. The stream can process only as much as it needs and then stop. Lazy fetching in JPA follows the same idea: associated data is queried when it is actually used.
- Lambda expression
- This is a syntax, introduced in Java 8, for writing a function with no name in a compact way. It has the form (parameters) -> body, and it is used as the implementation of a functional interface, which has only one abstract method. It lets you pass behavior (code) to another method as if it were a value.
- Optional
- This container class expresses in the type that a value may or may not be present. When a method returns it in place of null, the caller is guided to handle the case where there is no value. It is used mainly as a method return type.
Try these
- Default settings — One element goes through filter and map to the end, and then the stream moves on to the next element.
- Without a terminal operation — Neither filter nor map runs even once.
- findFirst() — The stream stops the moment the first element is found. It does not even look at the rest.
- limit(2) — Once two elements have been taken, the remaining elements are not handled.
- Adding sorted() — Sorting requires gathering all the elements, so filter runs to the end first.
Running with the default settings
- stream(), filter(), map() were called in order. These calls only link together "what to do". They do not take a single element out. No lambda expression has run yet.
- toList() was called. Execution starts now. The terminal operation pulls the elements through one at a time.
- filter(5): It is greater than 3, so it passes. It goes straight on to the next operation, map.
- map(5): The value is changed to 50 and put into the result.
- filter(2): It is not greater than 3, so it is dropped. This element does not go to map.
- filter(8): It is greater than 3, so it passes. It goes straight on to the next operation, map.
- map(8): The value is changed to 80 and put into the result.
- filter(1): It is not greater than 3, so it is dropped. This element does not go to map.
- filter(9): It is greater than 3, so it passes. It goes straight on to the next operation, map.
- map(9): The value is changed to 90 and put into the result.
- filter(4): It is greater than 3, so it passes. It goes straight on to the next operation, map.
- map(4): The value is changed to 40 and put into the result.
- Result: [50, 80, 90, 40]
Result of this run
- The stream does not "run filter on everything and then map on everything". One element goes all the way through filter → map, and only then does the next element set off. That is why the stream can stop partway.
Code
List.of(5, 2, 8, 1, 9, 4).stream()
.filter(x -> x > 3) // only values greater than 3
.map(x -> x * 10) // multiply by 10
.toList();
Assumptions of this model
The execution order and the result of all 12 combinations were checked against the result of running the same pipeline on JDK 21 on this PC. This applies to a sequential stream, and in a parallel stream the order in which the elements are handled is different.
Previous lab: Lambda expressions and functional interfaces · Next lab: Collecting a stream