Intermediate stream operations not evaluated on count

In jdk-9 it was clearly documented in java docs

The eliding of side-effects may also be surprising. With the exception of terminal operations forEach and forEachOrdered, side-effects of behavioral parameters may not always be executed when the stream implementation can optimize away the execution of behavioral parameters without affecting the result of the computation. (For a specific example see the API note documented on the count operation.)

API Note:

An implementation may choose to not execute the stream pipeline (either sequentially or in parallel) if it is capable of computing the count directly from the stream source. In such cases no source elements will be traversed and no intermediate operations will be evaluated. Behavioral parameters with side-effects, which are strongly discouraged except for harmless cases such as debugging, may be affected. For example, consider the following stream:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

The number of elements covered by the stream source, a List, is known and the intermediate operation, peek, does not inject into or remove elements from the stream (as may be the case for flatMap or filter operations). Thus the count is the size of the List and there is no need to execute the pipeline and, as a side-effect, print out the list elements.


The count() terminal operation, in my version of the JDK, ends up executing the following code:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

If there is a filter() operation in the pipeline of operations, the size of the stream, which is known initially, can't be known anymore (since filter could reject some elements of the stream). So the if block is not executed, the intermediate operations are executed and the StringBuilder is thus modified.

On the other hand, If you only have map()in the pipeline, the number of elements in the stream is guaranteed to be the same as the initial number of elements. So the if block is executed, and the size is returned directly without evaluating the intermediate operations.

Note that the lambda passed to map() violates the contract defined in the documentation: it's supposed to be a non-interfering, stateless operation, but it is not stateless. So having a different result in both cases can't be considered as a bug.


This is not what .map is for. It is supposed to be used to turn a stream of "Something" into a stream of "Something Else". In this case, you are using map to append a string to an external Stringbuilder, after which you have a stream of "Stringbuilder", each of which was created by the map operation appending one number to the original Stringbuilder.

Your stream doesn't actually do anything with mapped results in the stream, so it's perfectly reasonable to assume that the step can be skipped by the stream processor. You're counting on side effects to do the work, which breaks the functional model of the map. You'd be better served by using forEach to do this. Do the count as a separate stream entirely, or put a counter using AtomicInt in the forEach.

The filter forces it to run the stream contents since the it now has to do something notionally meaningful with each stream element.