Note that the behavior of
Set.of("A", "B").parallelStream().reduce("I", String::concat)
is not different to
Stream.concat(Stream.of("A", "B"), Stream.of()).parallel()
.reduce("I", String::concat)
or
Stream.of("A", "B", "none").parallel()
.filter(s -> !s.equals("none")).reduce("I", String::concat)
except for the unpredictability of the order of A and B which is irrelevant here.
Since the specified identity argument must be valid as result for processing an empty sequence of elements, as well as fulfilling the f(identity, element) = element
requirement, as explained in this answer, it follows that f(element, identity) = element
must be true as well, to ensure that the behavior of concatenating a sequence with an empty sequence has the same (or a compatible) result.
Why does that happen in practice, technically?
The Stream implementation has to make trade-offs to provide reasonable performance for arbitrary scenarios and one is to not invest too much effort into attempting balanced workload splitting if the source structure does not genuinely support it.
As explained in this answer, hash based collections tend to have larger arrays than their number of elements, to reduce the risk of collisions, and do not know in advance how the elements are distributed over the array when a stream is created. Therefore, the workload is split based on the array indices, similar to a random access list, but resulting ranges may have different numbers of elements in it or even have no elements at all.
This may result in the scenario of having all actual elements processed by a single thread as discussed in that answer, but it can also lead to threads processing empty spans which should not have an impact on the correctness of the result, as long as the identity chosen for the reduction is correct.
This processing is similar to the filter
scenario above.
However, when you’re using OpenJDK or a Java implementation based on it (or implemented the same way), this is not the case for Set.of("A", "B")
. This factory method returns a special implementation for a set of two elements, not using an array.
But, as surprising as it may sound, these new immutable collection implementations have no dedicated stream support. Instead of providing a Spliterator
implementation which could perfectly split into two spliterators of one element, this set inherits the default implementation based on an Iterator
. In order to be able to do parallel processing at all, the iterator based spliterator has to transfer the elements into an array first. In case of just two elements, this happens on the first split operation. Afterwards, there is an array based spliterator, which supports perfectly balanced splitting (for the subsequent split operations) and an empty spliterator.
So, in this case, the processing is rather similar to the Stream.concat(…)
example above.
This does not happen in case of List.of("A", "B")
. This implementation also has no dedicated Stream support but the default implementation for List
checks whether the list is a random access list and provides an indexed based implementation which can perform balanced splits without copying elements into an array.
This Q&A shows how you can use the behavior of this Stream implementation to visualize the way, the workload has been split.