Back to Blog
Java

Java Stream forEach: Usage, Pitfalls, and Performance

java stream foreach: Understand how to use Java Stream forEach effectively: syntax, side effects, performance tradeoffs, and common mistakes to avoid.

java streamsforEachlambda expressionsterminal operationsjava 8functional programming
Java stream forEach concept illustrated with a pipeline of elements flowing through a consumer action

java stream foreach requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

The forEach method is a terminal operation on a Java Stream that applies a given action to each element of the stream. It is the most direct way to iterate over a stream when the goal is to perform a side effect—such as printing, writing to a file, or updating a field—rather than to produce a new value. The method signature is void forEach(Consumer<? super T> action), and it consumes the stream entirely, meaning the stream cannot be reused afterward.

Syntax and Basic Usage

The simplest form uses a lambda expression as the Consumer. For example, given a list of order IDs, you might want to log each one:

List<String> orderIds = List.of("ORD-1001", "ORD-1002", "ORD-1003"); orderIds.stream().forEach(orderId -> System.out.println("Processing " + orderId));

This prints each ID to the console. The lambda orderId -> System.out.println(...) is a Consumer that accepts one argument and returns no result. You can also use a method reference when the action already exists as a method:

orderIds.stream().forEach(System.out::println);

Method references are often more readable when the action is a single existing method call. Both forms compile to the same bytecode, so the choice is stylistic.

How forEach Differs from Other Terminal Operations

forEach is one of several terminal operations, but it is unique in that it returns void. Other terminal operations like collect, reduce, or toList produce a result that can be used later. This distinction matters for pipeline design. If you need to transform the stream into a collection or aggregate a value, use collect or reduce. If you only need to perform an action on each element, forEach is appropriate.

Consider the difference:

// Collects into a new list List<String> upperCase = orderIds.stream() .map(String::toUpperCase) .collect(Collectors.toList()); // Prints each element, returns nothing orderIds.stream().forEach(System.out::println);

Using forEach when you actually need a result is a common mistake. The pipeline is consumed, and you are left with no output. Conversely, using collect when you only want side effects forces you to create an unnecessary intermediate structure.

Order and Parallel Streams

The order in which forEach processes elements depends on whether the stream is sequential or parallel. For a sequential stream, the action is applied in the encounter order of the stream source—the order in which elements appear in the underlying collection. For a parallel stream, the action may be applied concurrently on multiple threads, and the order is not guaranteed unless you explicitly use forEachOrdered.

List<Integer> numbers = List.of(1, 2, 3, 4, 5); numbers.parallelStream().forEach(n -> System.out.print(n + " ")); // Output can be any permutation, e.g., 3 1 4 2 5

If preserving order is important, use forEachOrdered instead:

numbers.parallelStream().forEachOrdered(n -> System.out.print(n + " ")); // Always prints 1 2 3 4 5

forEachOrdered sacrifices some parallelism because it must enforce the original order, but it is the correct choice when order matters for correctness (e.g., writing records to a file in sequence).

Side Effects and Shared Mutable State

The intended use of forEach is to perform side effects. However, you must be careful when the action modifies shared mutable state, especially in parallel streams. The Consumer is not guaranteed to be executed sequentially, and concurrent writes to the same variable can lead to race conditions.

// Unsafe in parallel streams List<Integer> values = List.of(1, 2, 3, 4, 5); int[] sum = {0}; values.parallelStream().forEach(v -> sum[0] += v);

This code is not thread-safe because the sum[0] += v operation is not atomic. Even in a sequential stream, relying on mutable state inside forEach makes the pipeline harder to parallelize later. Prefer a reduction operation like mapToInt().sum() for accumulation:

int safeSum = values.stream().mapToInt(Integer::intValue).sum();

If you must mutate a shared object, use a thread-safe collection or synchronize explicitly, but that usually defeats the purpose of using streams. The Java documentation itself warns against stateful lambdas in parallel streams.

Performance Considerations

forEach has a small overhead compared to a traditional for loop because of the lambda abstraction and the stream pipeline machinery. For most applications this overhead is negligible. However, in tight loops processing millions of elements, the difference can become measurable. The key cost is the creation of the Consumer object and the virtual call to the lambda, which may not be inlined as aggressively as a direct loop.

Parallel streams can improve throughput for CPU-bound operations, but they introduce thread scheduling and partitioning overhead. For small collections, the overhead often outweighs the benefit. A rule of thumb is to use parallel streams only when the dataset is large and the operation is independent per element. Also, be aware that parallel forEach does not guarantee order, as discussed earlier.

If performance is critical and the action is simple, a traditional enhanced for loop is often faster and clearer:

for (String orderId : orderIds) { System.out.println(orderId); }

There is no built-in benchmark that universally favors one approach; the difference depends on the JVM, the size of the stream, and the action complexity. The primary benefit of forEach is not speed but expressiveness and composability within a stream pipeline.

Common Pitfalls and Misuses

Several mistakes recur when developers use forEach.

Modifying the source collection inside the action can cause a ConcurrentModificationException if the stream is backed by a non-thread-safe collection and the modification changes its structure. For example, removing an element from a List while iterating is unsafe:

List<String> items = new ArrayList<>(List.of("a", "b", "c")); items.stream().forEach(item -> { if (item.equals("b")) { items.remove(item); // Throws ConcurrentModificationException } });

Use filter and collect to create a new list instead.

Throwing checked exceptions inside forEach is awkward because Consumer does not allow checked exceptions. The lambda cannot throw an IOException directly; you must wrap it in an unchecked exception or handle it inside the lambda. This often leads to verbose code and can hide errors if you simply catch and ignore them.

Null elements are allowed in streams unless the source forbids them. If your action dereferences the element without a null check, you will get a NullPointerException. Decide whether nulls are valid in your domain and handle them explicitly.

Using forEach for filtering or transformation is a misuse. Streams are designed to be declarative; filter, map, and collect express the intent more clearly. forEach should be reserved for terminal side effects, not for logic that could be part of the pipeline.

When to Prefer a Traditional Loop

A traditional for or enhanced for loop is often a better choice when:

  • You need to break out of the loop early or skip iterations based on an index.
  • You are working with a very large collection and the action is trivial.
  • You need to modify the collection being iterated (though this is generally discouraged).
  • You are writing code for a team that is not familiar with functional idioms and you want to minimize cognitive overhead.

On the other hand, forEach is preferable when you are already in a stream pipeline and want to apply a side effect after filter or map, or when you want to take advantage of parallel execution without writing explicit thread management.

Handling Checked Exceptions Inside forEach

A frequent practical problem is dealing with methods that throw checked exceptions, such as IOException when writing to a file. Since Consumer does not declare any checked exceptions, you cannot directly call such a method from a lambda. The typical workaround is to wrap the call in a try-catch and rethrow an unchecked exception, or to use a custom functional interface that allows checked exceptions.

List<Path> files = List.of(Path.of("a.txt"), Path.of("b.txt")); files.stream().forEach(file -> { try { Files.writeString(file, "content"); } catch (IOException e) { throw new UncheckedIOException(e); } });

This works but makes the lambda verbose. An alternative is to define a local helper method that handles the exception and returns a Consumer:

static Consumer<Path> writeFile(String content) { return file -> { try { Files.writeString(file, content); } catch (IOException e) { throw new UncheckedIOException(e); } }; } files.stream().forEach(writeFile("content"));

This keeps the stream pipeline clean and centralizes the exception handling. Be aware that throwing an unchecked exception from forEach will terminate the stream processing immediately; there is no built-in mechanism to continue after an error. If you need to collect failures and continue, consider using a custom Consumer that records errors, or use a loop with explicit error handling.

java stream foreach: Practical Usage and Code Examples | RYUSLOG DEV