Definition
- forEach(Consumer): terminal, executes action on each element. No ordering guarantee in parallel.
- forEachOrdered(Consumer): terminal, preserves encounter order even in parallel by serialising output.
- Sequential: both behave identically (encounter order preserved).
- Parallel forEach: elements processed in whichever order threads complete.
- Parallel forEachOrdered: threads wait for earlier elements to be processed first.
- Alternative: collect(toList()) preserves order with full parallelism benefit.
Eight clerks at National Bank of India process cheques simultaneously. forEach: each clerk shouts 'DONE!' as they finish, results in random order. forEachOrdered: clerks finish at different times, but results are announced in original cheque order. Clerk 3 waits for Clerks 1 and 2 to announce first. Same work done, but ordered announcement adds waiting time.
Understanding the Concepts
What is the difference between forEach and forEachOrdered?
Both are terminal operations that execute a Consumer on each element. The difference is ordering in parallel streams.
How do they behave differently?
package com.bank.core;
import java.util.List;
public class ForEachDemo {
public static void main(String[] args) {
List<String> accounts = List.of("SAV2001", "SAV2002", "CUR3001", "SAV2003", "CUR3002");
System.out.println("Sequential forEach:");
accounts.stream().forEach(System.out::println); // ordered
System.out.println("\nParallel forEach:");
accounts.parallelStream().forEach(System.out::println); // scrambled!
System.out.println("\nParallel forEachOrdered:");
accounts.parallelStream().forEachOrdered(System.out::println); // ordered
}
}
Sequential forEach:
SAV2001
SAV2002
CUR3001
SAV2003
CUR3002
Parallel forEach:
CUR3001
SAV2001
CUR3002
SAV2003
SAV2002
Parallel forEachOrdered:
SAV2001
SAV2002
CUR3001
SAV2003
CUR3002
Comparison table
| Aspect | forEach | forEachOrdered |
|--------|---------|---------------|
| Sequential | Ordered | Ordered |
| Parallel | Non-deterministic (fast) | Ordered (slower) |
| Use case | Logging, notifications | Audit trails, ordered output |
| Performance | Full parallel benefit | Serialised consumption |
When should you use which?
| Scenario | Use |
|----------|-----|
| Order does not matter (logging) | forEach |
| Order matters in parallel | forEachOrdered or collect(toList()) |
| Need ordered collection | collect(toList()) (preferred) |
What is the recommended pattern?
Prefer collect(toList()) over forEachOrdered for ordered parallel results. collect is designed for parallel accumulation with order preservation.
// Preferred: collect preserves order AND parallelism
List<String> ordered = accounts.parallelStream()
.filter(a -> a.startsWith("SAV"))
.toList(); // ordered, parallel-friendly
ordered.forEach(System.out::println);
Why does forEachOrdered reduce parallel performance?
forEachOrdered only serialises the final output step. Intermediate operations (filter, map) still run in parallel. The slowdown is in the output ordering: threads must wait for earlier elements to be consumed first.
// Processing is parallel, but output is sequential
accounts.parallelStream()
.map(id -> {
System.out.println(Thread.currentThread().getName() + " processes " + id);
return id.toUpperCase();
})
.forEachOrdered(System.out::println); // output in encounter order
What are common mistakes?
// WRONG: using forEach for ordered output from parallel stream
accounts.parallelStream()
.forEach(System.out::println); // scrambled order!
// WRONG: using forEachOrdered when order doesn't matter
accounts.parallelStream()
.forEachOrdered(a -> logger.info("Processing: {}", a)); // unnecessary serialisation
// CORRECT: use forEach when order doesn't matter
accounts.parallelStream()
.forEach(a -> logger.info("Processing: {}", a)); // full parallel benefit
What about void return type?
Both forEach and forEachOrdered return void. They are terminal operations for side effects, not for producing results. For producing results, use collect or reduce.
What should a senior engineer know?
- forEachOrdered only serialises the final output step. Intermediate operations still run in parallel.
- For most use cases, collect(toList()) followed by sequential forEach is the cleanest pattern.
- Use forEach for fire-and-forget side effects where order does not matter.
- In banking audit trails where order matters, use forEachOrdered or collect then iterate.
- forEach on a sequential stream always preserves encounter order.
Visual Overview
Parallel forEach: Parallel forEachOrdered: (no order guarantee) (encounter order preserved) ┌────────────────────┐ ┌────────────────────┐ │ Thread-1: CUR3001 │ prints │ Thread-1: CUR3001 │ waits │ Thread-2: SAV2001 │ prints │ Thread-2: SAV2001 │ prints first │ Thread-3: SAV2002 │ prints │ Thread-3: SAV2002 │ waits for T-2 │ Output: scrambled │ │ Output: ordered │ └────────────────────┘ └────────────────────┘
Key Takeaway
forEach and forEachOrdered both execute a Consumer on each element. In sequential streams, both preserve order. In parallel streams, forEach is non-deterministic (faster), forEachOrdered preserves encounter order (slower).
Prefer collect(toList()) over forEachOrdered for ordered results from parallel streams. collect is designed for parallel accumulation with order preservation.
Remember one sentence: forEach is unordered in parallel (fast), forEachOrdered preserves order (slower), and collect(toList()) is the best of both worlds.
---