Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stream.peek() can be skipped for optimization

I've come across a rule in Sonar which says:

A key difference with other intermediate Stream operations is that the Stream implementation is free to skip calls to peek() for optimization purpose. This can lead to peek() being unexpectedly called only for some or none of the elements in the Stream.

Also, it's mentioned in the Javadoc which says:

This method exists mainly to support debugging, where you want to see the elements as they flow past a certain point in a pipeline

In which case can java.util.Stream.peek() be skipped? Is it related to debugging?

like image 494
Eugene Mamaev Avatar asked Sep 03 '25 03:09

Eugene Mamaev


2 Answers

Not only peek but also map can be skipped. It is for sake of optimization. For example, when the terminal operation count() is called, it makes no sense to peek or map the individual items as such operations do not change the number/count of the present items.

Here are two examples:


1. Map and peek are not skipped because the filter can change the number of items beforehand.

long count = Stream.of("a", "aa")
    .peek(s -> System.out.println("#1"))
    .filter(s -> s.length() < 2)
    .peek(s -> System.out.println("#2"))
    .map(s -> {
        System.out.println("#3");
        return s.length();
    })
    .count();
#1
#2
#3
#1
1

2. Map and peek are skipped because the number of items is unchanged.

long count = Stream.of("a", "aa")
    .peek(s -> System.out.println("#1"))
  //.filter(s -> s.length() < 2)
    .peek(s -> System.out.println("#2"))
    .map(s -> {
        System.out.println("#3");
        return s.length();
    })
    .count();
2

Important: The methods should have no side-effects (they do above, but only for the sake of example).

Side-effects in behavioral parameters to stream operations are, in general, discouraged, as they can often lead to unwitting violations of the statelessness requirement, as well as other thread-safety hazards.

The following implementation is dangerous. Assuming callRestApi method performs a REST call, it won't be performed as the Stream violates the side-effect.

long count = Stream.of("url1", "url2")
    .map(string -> callRestApi(HttpMethod.POST, string))
    .count();
/**
 * Performs a REST call
 */
public String callRestApi(HttpMethod httpMethod, String url);
like image 126
Nikolas Charalambidis Avatar answered Sep 05 '25 22:09

Nikolas Charalambidis


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.

like image 30
Alexander Ivanchenko Avatar answered Sep 05 '25 21:09

Alexander Ivanchenko



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!