
Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:
Once the early-adopter seats are all used, the price will go up and stay at $33/year.
Last updated: January 8, 2024
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.
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.
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.
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.
In Java Streams, peek() and map() serve distinct purposes and should be used appropriately. map() is used for transforming the element such as for converting the type of element or changing the element:
List<Integer> integers = Arrays.asList(1, 2, 3, 4)
List<Integer> transformedElements = integers.stream()
.map(e -> e * 2)
.collect(Collectors.toList());
List<Integer> expected = Arrays.asList(2, 4, 6, 8);
assertEquals(expected, transformedElements)
peek() is mainly used for debugging or observing elements:
integers.stream()
.peek(System.out::println)
.collect(Collectors.toList());
Elements that are processed inside peek() might not be eligible for terminal operation:
List<Integer> peekedList = new ArrayList<>();
List<Integer> result = integers.stream()
.peek(peekedList::add)
.filter(e -> e < 3)
.collect(Collectors.toList());
assertEquals(Arrays.asList(1, 2), result);
assertEquals(Arrays.asList(1, 2, 3, 4), peekedList);
As we can see peekedList contains all the elements while the result only contains filtered results.
It’s tempting to use peek() to transform the elements, but we should always keep in mind that peek() doesn’t transform and pass the element downstream in a stream operation chain, unlike map(). Hence to transform the elements we should always use a map(), while with peek() we should use it for debugging or logging purposes.
In this short article, 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.