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.
- Print elements of a list.
- Save an object to a database.
- Modify an internal structure.
- Send data to an API.
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:
- Takes a
Consumer
— that is theaction
parameter. - Applies that
Consumer
to each element in the collection or stream. - Returns nothing — only performs the action on each element.
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
- Better performance for large collections (thousands/millions of elements).
- Ideal for independent and heavy tasks (data processing, complex calculations, analysis of large lists).
- Leverages multiple CPU cores without you having to manage threads manually.
When NOT to use it
- For small lists (splitting and coordination costs outweigh benefits).
- If output order is important (unless you use
forEachOrdered()
). - If operations are not thread-safe or depend on mutable shared state.
- On systems with few CPUs (little or no performance gain).
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