Java Stream peek
last modified May 24, 2025
This article demonstrates how to use the Java Stream peek method for debugging and inspecting stream elements during processing.
The peek method is an intermediate operation in the Java
Stream API that allows you to perform a non-interfering action on each element
of a stream as it passes through the pipeline, returning a new stream with the
same elements. Primarily designed for debugging, it accepts a
Consumer
function to observe or log elements without altering
them.
For example, you can use peek
to print intermediate values
or track the state of elements during complex stream operations. While its
main purpose is debugging, it can also support non-interfering side effects,
such as logging or metrics collection, provided these actions adhere to the
stream API's non-interference requirements. However, developers should use
peek
cautiously, as improper use (e.g., modifying elements or
introducing stateful behavior) can violate stream semantics and lead to
unpredictable results, especially in parallel streams.
Basic peek Syntax
The peek
method in the Java Stream API is defined with a single
method signature that enables inspection of stream elements through a provided
action. This signature is straightforward, making it easy to integrate into
stream pipelines for debugging or non-interfering side effects.
Stream<T> peek(Consumer<? super T> action)
The peek
method accepts a Consumer
, a functional
interface that takes a single input argument of type T
(or a
supertype) and returns no result. This Consumer
defines the action
to be performed on each stream element as it passes through the pipeline, such
as logging or inspecting its state. The peek
operation is
non-destructive, returning a new stream containing the same elements as the
original stream, ensuring the pipeline's data remains unchanged.
While primarily used for debugging (e.g., printing intermediate values), the
action must be non-interfering to comply with stream API requirements, meaning
it should not modify the stream's source or introduce stateful behavior.
Developers should exercise caution, especially in parallel streams, where side
effects from peek
could lead to unpredictable outcomes if not
properly managed.
Debugging stream operations
In the following example, we will use peek
to log elements
as they pass through the stream pipeline. This is useful for debugging
and understanding how data flows through the stream.
void main() { long count = Stream.of("apple", "banana", "cherry", "date") .peek(e -> System.out.println("Original: " + e)) .filter(s -> s.length() > 4) .peek(e -> System.out.println("Filtered: " + e)) .map(String::toUpperCase) .peek(e -> System.out.println("Mapped: " + e)) .count(); System.out.println("Total count: " + count); }
This example demonstrates how peek can be used to observe elements at different stages of stream processing. Each peek call logs the current state of elements.
$ java Main.java Original: apple Original: banana Filtered: banana Mapped: BANANA Original: cherry Filtered: cherry Mapped: CHERRY Original: date Total count: 2
Understanding lazy evaluation
peek
helps visualize stream's lazy evaluation behavior.
void main() { Stream.of("one", "two", "three", "four") .peek(e -> System.out.println("Before filter: " + e)) .filter(e -> e.length() > 3) .peek(e -> System.out.println("After filter: " + e)) .findFirst() .ifPresent(System.out::println); }
This example shows how streams process elements one at a time until the terminal operation is satisfied. The findFirst operation stops processing after finding the first matching element.
$ java Main.java Before filter: one Before filter: two Before filter: three After filter: three three
Tracking state changes
We can use peek
to observe state changes in mutable objects.
class Counter { int count = 0; void increment() { count++; } @Override public String toString() { return "Count: " + count; } } void main() { List<Counter> counters = new ArrayList<>(); for (int i = 0; i < 5; i++) { counters.add(new Counter()); }; counters.stream() .peek(c -> System.out.println("Before: " + c)) .forEach(Counter::increment); System.out.println("\nAfter processing:"); counters.forEach(System.out::println); }
This example shows how peek can observe state changes in mutable objects. Note that modifying objects in peek is generally discouraged as it can lead to unexpected behavior.
$ java Main.java Before: Count: 0 Before: Count: 0 Before: Count: 0 Before: Count: 0 Before: Count: 0 After processing: Count: 1 Count: 1 Count: 1 Count: 1 Count: 1
Logging transformations
Logging intermediate transformations in a complex pipeline.
record Product(String name, double price, int stock) { } void main() { Stream.of( new Product("Laptop", 999.99, 5), new Product("Phone", 699.99, 10), new Product("Tablet", 349.99, 0), new Product("Monitor", 249.99, 8) ) .peek(p -> System.out.println("Original: " + p)) .filter(p -> p.stock() > 0) .peek(p -> System.out.println("In stock: " + p)) .map(p -> new Product(p.name(), p.price() * 0.9, p.stock())) // 10% discount .peek(p -> System.out.println("Discounted: " + p)) .forEach(p -> System.out.println("Final: " + p)); }
This example shows how peek can help understand each transformation step in a more complex stream pipeline involving filtering and mapping.
Source
Java Stream peek documentation
In this article we have explored the Java Stream peek
method. While
primarily designed for debugging, it can be useful for observing stream
processing without modifying the stream contents. Remember that
peek
should generally not be used for operations that modify state
or affect the stream outcome.
Author
List all Java tutorials.