Fading Coder

An Old Coder’s Final Dance

Home > Tech > Content

Java Streams: Practical Differences Between forEach and peek for In-Place Updates

Tech 1

This guide illustrates how to update objects inside a list while still performing downstream stream operations, and clarifies how Stream.forEach, Collection.forEach, and Stream.peek differ.

Model used in examples

package com.example.model;

public class Person {
    private Integer id;
    private String fullName;
    private int age;

    public Person() {}

    public Person(String fullName, int age) {
        this.fullName = fullName;
        this.age = age;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFullName() {
        return fullName;
    }

    public void setFullName(String fullName) {
        this.fullName = fullName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", fullName='" + fullName + '\'' +
                ", age=" + age +
                '}';
    }
}

Test data and scenarios

import org.junit.jupiter.api.Test;

import java.util.*;
import java.util.stream.*;

public class StreamSideEffectTest {

    private List<Person> seed() {
        List<Person> data = new ArrayList<>();
        data.add(new Person("A", 28));
        data.add(new Person("B", 19));
        data.add(new Person("C", 33));
        data.add(new Person("D", 22));
        return data;
    }

    @Test
    void mutateThenMin_withCollectionForEach() {
        List<Person> people = seed();

        // Mutate first, outside any stream pipeline
        people.forEach(p -> p.setFullName(p.getFullName() + "_tag"));

        // Then create a fresh stream and compute the minimum by age
        Person youngest = people.stream()
                .min(Comparator.comparingInt(Person::getAge))
                .orElseThrow();

        System.out.println(youngest);
    }

    @Test
    void inlineMutate_withPeekThenMin() {
        List<Person> people = seed();

        // Perform the side-effect in the pipeline using peek, then find the min
        Person youngest = people.stream()
                .peek(p -> p.setFullName(p.getFullName() + "_tag"))
                .min(Comparator.comparingInt(Person::getAge))
                .orElseThrow();

        System.out.println(youngest);
    }

    @Test
    void streamForEach_isTerminalAndRequiresANewStream() {
        List<Person> people = seed();

        // Stream.forEach is a terminal operation; it consumes the stream
        people.stream().forEach(p -> p.setFullName(p.getFullName() + "_mut"));

        // You must obtain a new stream for further operations
        int minAge = people.stream()
                .mapToInt(Person::getAge)
                .min()
                .orElseThrow();

        System.out.println(minAge);
    }
}

What’s happening under the hood

  • Collection.forEach (e.g., list.forEach):

    • Iterates the collection and returns void.
    • Not part of a stream pipeline; you can create a stream afterward to further processing.
  • Stream.forEach:

    • Terminal operation that consumes the stream and returns void.
    • After calling it, you cannnot reuse the same stream; start a new stream from the source if you need more operatinos.
  • Stream.peek:

    • Intermediate operation that returns a new stream.
    • Intended primarily for observing elements in a pipeline; the action runs only when a terminal operation is invoked downstream.
    • If used to mutate elements, be cautious—especially with parallel streams—because side effects on shared state require proper synchronizaiton.

Notes on selecting the minimum element

  • Prefer concise comparators when possible:
    • Using Comparator.comparingInt(Person::getAge) is clearer and less error-prone than hand-written comparisons.
    • min(...).orElseThrow() is a straightforward way to unwrap the Optional when you expect at least one element.

API references (signatures)

  • Iterable (e.g., List):
    • default void forEach(Consumer<? super T> action)
  • Stream:
    • void forEach(Consumer<? super T> action) // terminal
    • Stream<T> peek(Consumer<? super T> action) // intermediate

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.