Learnitweb

How does Stream.peek and forEach differ in behavior

The methods Stream.peek() and Stream.forEach() in Java Streams are superficially similar—they both accept a Consumer and can execute code on each element—but they differ fundamentally in purpose, stream behavior, and best practices.

Key Differences

Aspectpeek()forEach()
TypeIntermediate operationTerminal operation
Stream ContinuityReturns a new Stream (can chain more ops)Consumes the Stream (cannot reuse)
Typical Intended UseDebugging, inspection, loggingFinal processing (actions with side effects)
Execution TimingDeferred—executes when terminal op is calledExecutes immediately as terminal action
Stream Pipeline ImpactDoes not terminate streamEnds the stream pipeline
Mutating ElementsNot recommended (can cause issues)Allowed, but use with caution
Recommended UsageFor debugging and non-interfering actionsFor applying final side-effect logic

Behavioral Explanation

  • peek() is designed as an intermediate operation, meaning it is meant to be used within a stream pipeline to observe (peek at) each element as it passes through. It is primarily for debugging or logging. For example, you might use peek() to print out an element after filtering but before mapping. It returns a Stream, so you can continue chaining further operations after it. Since it is not a terminal operation, nothing inside peek() actually executes until a terminal operation (like collectforEach, etc.) is called.
  • forEach() is a terminal operation, meaning it concludes the use of the stream. When you call forEach(), the entire pipeline executes and the stream is consumed. This is where you apply a side-effecting action to every element—printing, saving to DB, etc. Once forEach() has run, you cannot use the stream any further.

Example

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// Using peek to observe intermediate values
numbers.stream()
    .filter(n -> n % 2 == 0)
    .peek(n -> System.out.println("Filtered: " + n))
    .map(n -> n * n)
    .peek(n -> System.out.println("Mapped: " + n))
    .collect(Collectors.toList());

// Using forEach to apply a side-effect
numbers.stream()
    .forEach(n -> System.out.println("ForEach: " + n));

Output example:

Filtered: 2
Mapped: 4
Filtered: 4
Mapped: 16
ForEach: 1
ForEach: 2
ForEach: 3
ForEach: 4
ForEach: 5
  • In the first block, peek() lines print values only when the terminal operation (collect) is invoked, allowing you to observe stream transformations step-by-step.
  • In the second block, forEach() directly prints every element of the stream as the final action.

Practical Guidelines

  • Use peek() only for observing or debugging elements as they flow through a stream. Avoid side effects or mutations within peek(), especially on parallel streams, as order and thread context are not guaranteed and may lead to unpredictable results.
  • Use forEach() for side-effecting operations you want to apply at the end of a pipeline. forEach() is typically for actions such as printing results or saving entities.

Cautions

  • Modifying stream elements inside peek() is discouraged and can be regarded as an anti-pattern. If you need to transform elements, use map() instead.
  • In parallel streams, both peek() and forEach() can behave non-deterministically with respect to order and thread-safety. Side effects should be stateless or properly synchronized if unavoidable.