Guide to Java Stream API: Stream Execution Order
Now that we’ve learned how to create and work with different types of streams, let’s dive deeper into how to stream operations are processed.
An important characteristic of intermediate operations is laziness. If we execute the code without foreach
it won’t print anything. That is because intermediate operations will only be executed when a terminal operation is present.
Stream.of("a2", "a1", "b1", "b3", "c2")
.filter(s -> {
System.out.println("filter: " + s);
return true;
})
.forEach(s -> System.out.println("forEach: " + s));//filter: a2
//forEach: a2
//filter: a1
//forEach: a1
//filter: b1
//forEach: b1
//filter: b3
//forEach: b3
//filter: c2
//forEach: c2
The order of the result is possibly unexpected. A naive approach would be to perform the operations horizontally, one after another, on all elements of the stream. But instead, vertically, each element moves along the chain. The first string “a2” passes filter
then forEach
, only then the second string "a1" is processed.
Why the order of execution is important
It is very important to the order of intermediate operations used. By changing the order of operations, the number of executions can be decreased. Let’s once again inspect how those operations are being executed:
Stream.of("a2", "a1", "b1", "b3", "c2")
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("A");
})
.forEach(s -> System.out.println("forEach: " + s));//map: a2
//filter: A2
//forEach: A2
//map: a1
//filter: A1
//forEach: A1
//map: b1
//filter: B1
//map: b3
//filter: B3
//map: c2
//filter: C2
As you might have guessed both map
and filter
are called five times for every string in the underlying collection whereas forEach
is only called once.
The actual number of executions can be significantly reduced if we change the order of the operations, moving the filter
to the beginning of the stream.
Stream.of("a2", "a1", "b1", "b3", "c2")
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));//filter: a2
//map: a2
//forEach: A2
//filter: a1
//map: a1
//forEach: A1
//filter: b1
//filter: b3
//filter: c2
Now the map
is only called once, so the operation pipeline for massive volumes of stream elements performs much faster. When composing a complex operation, keep that in mind.
Let’s see another complex intermediate operation :sorted
Stream.of("a2", "a1", "b1", "b3", "c2")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));//sort: a1; a2
//sort: b1; a1
//sort: b1; a2
//sort: b3; a2
//sort: b3; b1
//sort: c2; b1
//sort: c2; b3
//filter: a1
//map: a1
//forEach: A1
//filter: a2
//map: a2
//forEach: A2
//filter: b1
//filter: b3
//filter: c2
The sorting operation is a so-called stateful operation because you have to maintain a state to sort a set of elements. First, the sort operation is executed on the whole collection of inputs.
Once again we can optimize the performance by reordering the stream:
Stream.of("a2", "a1", "b1", "b3", "c2")
.sorted((s1, s2) -> {
System.out.printf("sort: %s; %s\n", s1, s2);
return s1.compareTo(s2);
})
.filter(s -> {
System.out.println("filter: " + s);
return s.startsWith("a");
})
.map(s -> {
System.out.println("map: " + s);
return s.toUpperCase();
})
.forEach(s -> System.out.println("forEach: " + s));//filter: a2
//filter: a1
//filter: b1
//filter: b3
//filter: c2
//sort: a1; a2
//map: a1
//forEach: A1
//map: a2
//forEach: A2
As you can see sorting operations are less executed. So the performance is greatly increased for larger input collections.
In my next article, I will talk about advanced stream operations.