I have a teammate that did all his stream operations in a foreach execution. I tried to come up with advantages of using the filter, map and to list methods instead.
What are other advantages it has other than readability (arguably the foreach is pretty readable as well) Here is a snippet for something close to what he did:
List<Integer> firstInts = new ArrayList<>();
List<Integer> secondInts = new ArrayList<>();
List<Integer> numbersList = IntStream.range(0, max).boxed().toList();
//his stream
numbersList.stream()
.forEach(i -> {
if(i % 6 != 0) {
return;
}
secondInts.add(i);
});
//alternative 1
numbersList.stream()
.filter(i -> i % 6 == 0)
.forEach(firstInts::add);
//alternative 2
List<Integer> third = numbersList.stream()
.filter(i -> i % 6 == 0)
.toList();
what is the motivation of using stream methods other than readability?
return a list containing number divisible by 6 from numList
, while forEach approach means add number divisible by 6 from numList to secondInts
filter(i -> i % 6 == 0)
is straight forward and
if(i % 6 != 0) {
return;
}
require some time for human brain to process.From Stream.toList()
Implementation Note: Most instances of Stream will override this method and provide an implementation that is highly optimized compared to the implementation in this interface.
We benefit from optimization from JDK by using Stream API.
And in this case, using forEach and adding element one by one will be slower, especially when the list is large. It is because ArrayList
will need to extend it capacity whenever the list full, while Stream implementation ImmutableCollections.listFromTrustedArrayNullsAllowed
just store the result array into ListN
.
One more point to note about parallelism:
From Stream#forEach
The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action may be performed at whatever time and in whatever thread the library chooses. If the action accesses shared state, it is responsible for providing the required synchronization.
numbersList.stream().parallel()
.forEach(i -> {
if(i % 6 != 0) {
return;
}
secondInts.add(i);
});
Will provide unexpected result, while
List<Integer> third = numbersList.stream().parallel()
.filter(i -> i % 6 == 0).sorted().forEach()
.toList();
is totally fine.
Imagine you want the filtered list to be sorted, in forEach approach, you can do it like:
numbersList.stream().sorted().
.forEach(i -> {
if(i % 6 != 0) {
return;
}
secondInts.add(i);
});
Which is much slower compared to
numbersList.stream()
.filter(i -> i % 6 == 0)
.sorted()
.toList();
As we need to sort the whole numbersList instead of filtered.
Or if you want to limit your result to 10 elements, it is not straight forward to do so with forEach, but just as simple as adding limit(10)
when using stream.
Stream API usually return Immutable object by default.
From Stream.toList()
Implementation Requirements: The implementation in this interface returns a List produced as if by the following: Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))
Meaning that the returned list is immutable by default. Some advantages of immutability are:
Read Pros. / Cons. of Immutability vs. Mutability for further discussion.