Back to Blog
Java

Understanding Java Stream forEachOrdered

Explains java stream foreachordered: encounter order guarantees, the difference from forEach in parallel streams, and when the ordering cost is worth paying.

Java StreamsParallel StreamsStream OrderingEncounter OrderTerminal Operations
Illustration of ordered element processing in a Java parallel stream, showing elements flowing through a pipeline in sequence.

When you call java stream foreachordered, you are asking the terminal operation to invoke an action on each element in the stream's encounter order. The method exists because the more common forEach does not guarantee that order, and the difference becomes visible the moment you switch to a parallel stream.

The Difference Between forEach and forEachOrdered

Both forEach and forEachOrdered are terminal operations that consume a stream and apply a Consumer to each element. The signatures are identical:

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

The difference is in the contract. forEach makes no promise about the order in which the action is invoked. forEachOrdered guarantees that the action runs in encounter order — the order in which elements appear in the stream source, assuming the stream has a defined encounter order.

In a sequential stream, both methods usually behave identically because elements are processed one at a time. The distinction only becomes meaningful when the stream is parallel.

How Ordering Behaves in Sequential Streams

A sequential stream processes its pipeline stage by stage, and each element flows through the pipeline before the next one starts. Because there is only one thread, the action passed to forEachOrdered is naturally invoked in source order. The same is true for forEach in most cases.

Consider this example:

List<String> names = List.of("alice", "bob", "carol"); names.stream() .map(String::toUpperCase) .forEach(System.out::println);

The output is ALICE, BOB, CAROL whether you use forEach or forEachOrdered, because the sequential pipeline preserves order by default. There is no observable difference between the two methods here.

Why Parallel Streams Change the Equation

When you call .parallel() on a stream, the runtime splits the source into chunks and processes them across multiple threads. The pipeline stages — map, filter, and so on — run concurrently. The terminal operation then consumes the results.

With forEach, the action may be invoked from multiple threads at once, and the order in which elements reach the action is not deterministic. The output of the following code can vary between runs:

List<String> names = List.of("alice", "bob", "carol", "dave", "eve"); names.parallelStream() .map(String::toUpperCase) .forEach(System.out::println);

One run might print ALICE, BOB, CAROL, DAVE, EVE. Another might print CAROL, ALICE, EVE, BOB, DAVE. The order depends on thread scheduling and how the source is split.

If the order of the output matters — for example, when writing results to a log file that must match the input sequence — forEach is not safe. Replacing it with forEachOrdered guarantees the encounter order:

names.parallelStream() .map(String::toUpperCase) .forEachOrdered(System.out::println);

This always prints the names in source order, regardless of how many threads process the pipeline.

What Encounter Order Actually Means

Encounter order is not the same as "the order you inserted elements." It is the order defined by the stream source. For a List, encounter order is the list's iteration order. For an array, it is index order. For a HashSet, there is no defined encounter order, so forEachOrdered has nothing to guarantee — the elements may come out in any order because the source itself has no order.

This distinction matters when you build a stream from an unordered source. Calling forEachOrdered on a stream backed by a HashSet does not restore insertion order, because the source never had one. To get deterministic output from an unordered source, you must sort the stream first, which is a separate operation with its own cost.

Performance Cost of forEachOrdered

The ordering guarantee does not come for free. In a parallel stream, forEachOrdered must coordinate the action invocation so that elements are consumed in encounter order. This coordination acts as a synchronization point at the terminal stage. The upstream pipeline — map, filter, flatMap — can still run in parallel, but the terminal action is effectively serialized.

forEach, by contrast, lets each worker thread invoke the action on its own chunk immediately, with no ordering constraint. For actions that are cheap and where order is irrelevant, forEach can complete noticeably faster on large streams.

The practical guidance is simple: if the action has a side effect that must be observed in source order, use forEachOrdered and accept the serialization cost. If order does not matter — for example, when writing to a concurrent collection or incrementing a counter — use forEach to avoid the ordering overhead.

Choosing Between forEach and forEachOrdered

The decision is driven by two questions: whether the stream is parallel, and whether the action's side effects must be observed in encounter order.

SituationRecommended method
Sequential stream, order irrelevantEither
Sequential stream, order mattersEither (both preserve order)
Parallel stream, order irrelevantforEach
Parallel stream, order mattersforEachOrdered
Unordered source (e.g., HashSet)forEach

For sequential streams, the choice has no practical effect on output. For parallel streams, forEachOrdered is the only way to guarantee encounter order at the terminal operation.

One common mistake is assuming that forEachOrdered makes the entire pipeline sequential. It does not. The pipeline stages still run in parallel; only the terminal action is ordered. If you need the pipeline itself to run sequentially, use .sequential() or avoid .parallel() entirely.

A Practical Example: Ordered Log Output

Suppose you have a list of request IDs and you want to log the processing result for each one in the same order the requests arrived. With a parallel stream, forEach would interleave log lines unpredictably. forEachOrdered keeps the log readable:

List<String> requestIds = fetchRequestIds(); requestIds.parallelStream() .map(this::processRequest) .forEachOrdered(result -> log.info("Result for {}: {}", result.id(), result.status()));

The map stage benefits from parallel execution, while the logging action is invoked in the original request order. This is the typical use case for forEachOrdered: parallel computation with ordered consumption of results.

If the log order did not matter, switching to forEach would remove the ordering constraint and allow the logging action to run concurrently on each worker thread. For a high-volume log stream, that difference can be significant.

java stream foreachordered: Practical Usage and Code Example | RYUSLOG DEV