peek()
is an intermediate operation, and it expects a consumer which perform an action (side-effect) on elements of the stream.
In case when a stream pipe-line doesn't contain intermediate operations which can change the number of elements in the stream, like takeWhile
, filter
, limit
, etc., and ends with terminal operation count()
and when the stream-source allows evaluating the number of elements in it, then count()
simply interrogates the source and returns the result. All intermediate operations get optimized away.
Note: this optimization of count()
operation, which exists since Java 9 (see the API Note), is not directly related to peek()
, it would affect every intermediate operation which doesn't change the number of elements in the stream (for now these are map()
, sorted()
, peek()
).
There's More to it
peek()
has a very special niche among other intermediate operations.
By its nature, peek()
differs from other intermediate operations like map()
as well as from the terminal operations that cause side-effects (like peek()
does), performing a final action for each element that reaches them, which are forEach()
and forEachOrdered()
.
The key point is that peek()
doesn't contribute to the result of stream execution. It never affects the result produced by the terminal operation, whether it's a value or a final action.
In other words, if we throw away peek()
from the pipeline, it would not affect the terminal operation.
Documentation of the method peek()
as well the Stream API documentation warns its action could be elided, and you shouldn't rely on it.
A quote from the documentation of peek()
:
In cases where the stream implementation is able to optimize away the
production of some or all the elements (such as with short-circuiting
operations like findFirst, or in the example described in count()),
the action will not be invoked for those elements.
A quote from the API documentation, paragraph Side-effects:
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.
Here's an example of the stream (link to the source) where none of the intermediate operations gets elided apart from peek()
:
Stream.of(1, 2, 3)
.parallel()
.peek(System.out::println)
.skip(1)
.map(n -> n * 10)
.forEach(System.out::println);
In this pipe-line peek()
presides skip()
therefor you might expect it to display every element from the source on the console. However, it doesn't happen (element 1
will not be printed). Due to the nature of peek()
it might be optimized away without breaking the code, i.e. without affecting the terminal operation.
That's why documentation explicitly states that this operation is provided exclusively for debugging purposes, and it should not be assigned with an action which needs to be executed at any circumstances.
Stream.peek() A key difference with other intermediate Stream operations is that the Stream implementation is free to skip calls to peek() for optimization purpose.
is wrong..peek
is not treated specially from.map
operator where the following example showcases this:Stream.of("A", "B", "C", "D").map(a ->{System.out.println(a);return a; }).count();
Here themap
operator is skipped as well. This is just an optimization of Streams not related with specific operator peek. – Tardifpeak
. But this is important to understand to not fall in the trap that Sonar reports in this example. – Tardif