Search code examples
javajava-8sonarqubejava-streamside-effects

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?


Solution

  • 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);