peek?

Why does the documentation recommend using Stream.peek mainly for debugging? Let’s find out.

Stream API

Since peek is part of the Stream API, let’s start by looking at the Stream API documentation.

According to the Stream API documentation, functions passed to methods like filter and map must satisfy two constraints for correct operation:

  • They must be non-interfering
  • In most cases, they should be stateless

Let’s focus on non-interfering.

non-interfering

Dictionary meaning of interfering

Interfering, meddlesome

What non-interfering means in the documentation

It means that the data source must not be modified at all during the execution of the stream pipeline. - Reference

If you violate this:

  • Unintended results may occur
  • Exceptions may be thrown

Based on this, let’s look at how peek is described in the Java docs.

Peek

Documentation link

img

Here are two things to note:

In other words, you must not modify the source data in a peek operation.

Why is being an intermediate operation important? Let’s look at some examples.

Examples

Let’s see if the source data is actually modified and what unintended results peek can cause.

Modifying the source data

Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
void test() {
    List<Wrapper> origin = List.of(new Wrapper(1L), new Wrapper(2L));

    List<Wrapper> modified = origin.stream()
        .peek(h -> h.setA(4L))
        .collect(Collectors.toList());

    assertThat(origin).isNotEqualTo(modified);
}

public static class Wrapper {
    private Long a;

    public void setA(Long a) {this.a = a;}

    public Wrapper(Long a) {this.a = a;}

    @Override
    public String toString() {
        return "Wrapper{" +
            "a=" + a +
            '}';
    }
}

Result

the value of the elements in the origin list (the data source) was changed.

peek and count - 1

Code

What will the following code print to the console?

1
2
3
4
5
6
7
8
@Test
void test2() {
    long count = IntStream.range(1, 10)
        .peek(System.out::println)
        .count();

    assertThat(count).isEqualTo(9);
}

Result

There is no output. In Java 9, due to optimization of the terminal operation count, the function passed to peek is not executed.

peek does not affect the count, so the function for standard output is not executed.

In other words, intermediate operations may not be executed for optimization (see lazy evaluation).

peek and count - 2

Code

1
2
3
4
5
6
7
8
9
@Test
void test3() {
    long count = IntStream.range(1, 10)
        .filter(n -> n % 2 == 0)
        .peek(System.out::println)
        .count();

    assertThat(count).isEqualTo(4);
}

Result

Since filter affects the result of count, peek is evaluated in this case.

Caution

Here are some points to keep in mind when using peek in development:

  • peek is an intermediate operation, so it may not be evaluated depending on the terminal operation (due to lazy evaluation and optimization)
  • Using peek differently from the documentation may cause problems in future versions (e.g., count() optimization from Java 8 to Java 9)

Finally, let’s look at methods with different paradigms and wrap up.

Stream.forEach vs Iterable.forEach

Although they have the same method name, Stream.forEach is made for the functional paradigm, while Iterable.forEach is for the procedural paradigm. Let’s see how their documentation differs.

Stream.forEach also specifies that you should pass a non-interfering action - link

Iterable.forEach only warns that modifying the source element may cause side effects - link

Thoughts - Expression

peek and forEach can both cause side-effects.

  • Even standard output is defined as a side effect. Among side effects, you should implement non-interfering actions that do not modify the data source.