Consumer<T>, ForEach, Peek

In Java, Consumer<T> is a functional interface that represents an operation that takes a single argument but does not return a result. It belongs to the java.util.function package (introduced in Java 8). It is also “functional” because it has only one abstract method.

void accept(T t);

What is it for?

It is used to perform actions on a value without returning anything.

Basic example

import java.util.function.Consumer;

public class ConsumerExample {
    public static void main(String[] args) {

        Consumer<String> consumer = (s) -> {
            System.out.println(s);
        };

        consumer.accept("These are the ... of King Kong");
    }
}

A consumer can be created in different ways; we can shorten it even more:

Consumer<String> consumer = s -> System.out.println(s);
// OR method reference
Consumer<String> consumer = System.out::println;

Real usage with lists

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class UserList {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Anna", "Luis", "Carlos");

        // Using explicit Consumer
        Consumer<String> printName = name -> System.out.println("Hello " + name);
        names.forEach(printName);

        // Or directly with lambda
        names.forEach(name -> System.out.println("Hello " + name));
    }
}

forEach()

In the example above we use the forEach method. This is defined in the Iterable interface and also in streams:

void forEach(Consumer<? super T> action);

How it works:

How it works internally

When you call:

list.forEach(element -> System.out.println(element));

Internally it is similar to:

for (T element : list) {
    action.accept(element);
}

Where action is the Consumer you passed.

forEach in collections vs streams

forEach in collections is defined by the Iterable interface. It iterates over elements in the order returned by the collection (normally insertion order) and does not change the order even if the collection is modified later.

List<String> list = Arrays.asList("A", "B", "C");
list.forEach(System.out::println);

// Output:
// A
// B
// C

forEach in streams can be ordered or unordered. If it is a sequential and ordered stream (like one obtained from a List), forEach respects encounter order. In a parallel stream (parallelStream()), forEach does not guarantee order. To preserve order in a parallel stream, use forEachOrdered().

List<String> list = Arrays.asList("A", "B", "C", "D");

System.out.println("Parallel forEach:");
list.parallelStream().forEach(System.out::println);

System.out.println("Parallel forEachOrdered:");
list.parallelStream().forEachOrdered(System.out::println);

// Parallel forEach:
// C
// A
// D
// B

// Parallel forEachOrdered:
// A
// B
// C
// D

Advantage of using parallelStream()

A sequential stream (stream()) processes elements one by one in a single thread.

A parallel stream (parallelStream()) splits the dataset into multiple chunks and processes them in multiple threads using the common ForkJoinPool by default. The result is then combined automatically.

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

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = IntStream.rangeClosed(1, 10).boxed().toList();

        System.out.println("Sequential:");
        numbers.stream()
               .forEach(n -> System.out.println(Thread.currentThread().getName() + " - " + n));

        System.out.println("\nParallel:");
        numbers.parallelStream()
               .forEach(n -> System.out.println(Thread.currentThread().getName() + " - " + n));
    }
}

Advantages

When NOT to use it

peek()

Just like forEach, peek() is an intermediate operation that takes a Consumer<? super T> and returns the same Stream after performing the action on each element.

Stream<T> peek(Consumer<? super T> action)

It does not consume the Stream, unlike forEach(), which is terminal. It is mainly used for debugging or performing secondary actions while the pipeline flows.

import java.util.stream.Stream;

public class PeekExample {
    public static void main(String[] args) {
        Stream.of("Java", "Python", "Go")
              .peek(lang -> System.out.println("Before map: " + lang))
              .map(String::toUpperCase)
              .peek(lang -> System.out.println("After map: " + lang))
              .forEach(System.out::println);
    }
}

Why doesn’t it work alone?

Stream.of("A", "B", "C")
      .peek(System.out::println);
// Prints nothing

It prints nothing because peek requires a terminal operation (forEach, collect, reduce, etc.) for the Stream to execute. This is due to the lazy evaluation of Streams.

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream.of("A", "B", "C")
              .peek(s -> System.out.println("Peek: " + s))
              .forEach(System.out::println); // terminal op
    }
}

// Output:
// Peek: A
// A
// Peek: B
// B
// Peek: C
// C