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
| Aspect | peek() | forEach() |
| Type | Intermediate operation | Terminal operation |
| Stream Continuity | Returns a new Stream (can chain more ops) | Consumes the Stream (cannot reuse) |
| Typical Intended Use | Debugging, inspection, logging | Final processing (actions with side effects) |
| Execution Timing | Deferred—executes when terminal op is called | Executes immediately as terminal action |
| Stream Pipeline Impact | Does not terminate stream | Ends the stream pipeline |
| Mutating Elements | Not recommended (can cause issues) | Allowed, but use with caution |
| Recommended Usage | For debugging and non-interfering actions | For 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 insidepeek()actually executes until a terminal operation (likecollect,forEach, 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. OnceforEach()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 withinpeek(), 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, usemap()instead. - In parallel streams, both
peek()andforEach()can behave non-deterministically with respect to order and thread-safety. Side effects should be stateless or properly synchronized if unavoidable.
