Course – LS – All
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

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 used in another scenario: 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);

Although it looks convenient, map() would be more appropriate here since using peek() with side effects goes against its intended use.

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.

Course – LS – All
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

res – REST with Spring (eBook) (everywhere)