Java 8 Streams peek() API

1. Introduction

The Java Stream API introduces us to a powerful alternative for processing data.

In this short tutorial, we’ll focus on peek(), an often misunderstood method.

2. Quick Example

Let’s get our hands dirty and try to use peek(). We have a stream of names, and we want to print them to the console.

Since peek() expects a Consumer<T> as its only argument, it seems like a good fit, so let’s give it a try:

Stream<String> nameStream = Stream.of("Alice", "Bob", "Chuck");
nameStream.peek(System.out::println);

However, the snippet above produces no output. To understand why, let’s do a quick refresher on aspects of the stream lifecycle.

3. Intermediate vs. Terminal Operations

Recall that streams have three parts: a data source, zero or more intermediate operations, and zero or one terminal operation.

The source provides the elements to the pipeline.

Intermediate operations get elements one-by-one and process them. All intermediate operations are lazy, and, as a result, no operations will have any effect until the pipeline starts to work.

Terminal operations mean the end of the stream lifecycle. Most importantly for our scenario, they initiate the work in the pipeline.

4. peek() Usage

The reason peek() didn’t work in our first example is that it’s an intermediate operation and we didn’t apply a terminal operation to the pipeline. Alternatively, we could have used forEach() with the same argument to get the desired behavior:

Stream<String> nameStream = Stream.of("Alice", "Bob", "Chuck");
nameStream.forEach(System.out::println);

peek()‘s Javadoc page 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“.

Let’s consider this snippet from the same Javadoc page:

Stream.of("one", "two", "three", "four")
  .filter(e -> e.length() > 3)
  .peek(e -> System.out.println("Filtered value: " + e))
  .map(String::toUpperCase)
  .peek(e -> System.out.println("Mapped value: " + e))
  .collect(Collectors.toList());

It demonstrates, how we observe the elements that passed each operation.

On top of that, peek() can be useful in another scenario: when we want to alter the inner state of an element. For example, let’s say we want to convert all user’s name to lowercase before printing them:

Stream<User> userStream = Stream.of(new User("Alice"), new User("Bob"), new User("Chuck"));
userStream.peek(u -> u.setName(u.getName().toLowerCase()))
  .forEach(System.out::println);

Alternatively, we could have used map(), but peek() is more convenient since we don’t want to replace the element.

5. Conclusion

In this short tutorial, we saw a summary of the stream lifecycle to understand how peek() works. We also saw two everyday use cases when using peek() is the most straightforward option.

And as usual, the examples are available over on GitHub.