Ten Reasons to Prefer Traditional for Loop Over Stream.forEach for List Traversal in Java
Through benchmark tests, memory analysis, and code examples, this article presents ten compelling reasons why using a traditional for loop to traverse Java Lists often outperforms Stream.forEach in terms of performance, memory usage, control flow, exception handling, mutability, debugging, readability, and state management.
Introduction
Many articles mock Java developers for still using a for loop to iterate over a List , claiming that the modern Stream.forEach() is superior. This article examines whether for is truly better and lists ten reasons to prefer for over Stream.forEach() .
Reason 1 – Better Performance
Benchmark tests using JMH compare a simple for loop with Stream.forEach() on a List<Integer> of various sizes. The test code is shown below:
@State(Scope.Thread)
public class ForBenchmark {
private List
ids;
@Setup
public void setup() {
ids = new ArrayList<>();
// populate 10, 100, 1k, 10k, 100k elements
IntStream.range(0, 10).forEach(i -> ids.add(i));
}
@Benchmark
public void testFor() {
for (int i = 0; i < ids.size(); i++) {
Integer id = ids.get(i);
}
}
@Benchmark
public void testStreamforEach() {
ids.stream().forEach(x -> {
Integer id = x;
});
}
@Test
public void testMyBenchmark() throws Exception {
Options options = new OptionsBuilder()
.include(ForBenchmark.class.getSimpleName())
.forks(1)
.threads(1)
.warmupIterations(1)
.measurementIterations(1)
.mode(Mode.Throughput)
.build();
new Runner(options).run();
}
}The JMH results (ops/s) are summarized in the table:
Method
10
100
1k
10k
100k
forEach
45,194,532
17,187,781
2,501,802
200,292
20,309
for
127,056,654
19,310,361
2,530,502
202,632
19,228
Improvement
+181%
+12%
+1%
-1%
-5%
For small lists (up to 10 k elements) the for loop is noticeably faster; only when the list exceeds 100 k elements does Stream.forEach() catch up.
In small lists (≤10 k elements) for outperforms Stream.forEach() .
Reason 2 – Lower Memory Consumption
Stream.forEach() creates additional objects (the stream, intermediate containers), leading to higher heap usage. GC logs from two runs illustrate the difference.
Using for : List ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList()); int sum = 0; for (int i = 0; i < ids.size(); i++) { sum += ids.get(i); } System.gc(); // GC log shows 392 540K used, 0.20 s pause
Using stream : List ids = IntStream.range(1,10000000).boxed().collect(Collectors.toList()); int sum = ids.stream().reduce(0, Integer::sum); System.gc(); // GC log shows 539 341K used, 0.38 s pause
The for version uses about 37 % less memory and finishes GC 85 % faster.
Reason 3 – Easier Control Flow
A for loop can use break , continue and return to exit or skip iterations, which is impossible inside Stream.forEach() . The following example shows that a return inside forEach does not stop the loop.
List
ids = IntStream.range(1,4).boxed().collect(Collectors.toList());
ids.stream().forEach(i -> {
System.out.println("forEach-"+i);
if (i > 1) {
return; // loop continues
}
});
System.out.println("==");
for (int i = 0; i < ids.size(); i++) {
System.out.println("for-"+ids.get(i));
if (ids.get(i) > 1) {
return; // loop stops
}
}Output demonstrates the difference.
Reason 4 – More Flexible Variable Access
Variables used inside Stream.forEach() must be effectively final, preventing modification of external state. With a for loop you can freely update variables such as a running sum.
int sum = 0;
for (int i = 0; i < ids.size(); i++) {
sum++;
}
// Stream version would not compile unless sum is wrapped in an AtomicReferenceReason 5 – Simpler Exception Handling
Inside a for loop you can let checked exceptions propagate, whereas Stream.forEach() forces you to catch or wrap them.
for (int i = 0; i < ids.size(); i++) {
System.out.println(div(i, i - 1)); // may throw
}
ids.stream().forEach(x -> {
try {
System.out.println(div(x, x - 1));
} catch (Exception e) {
throw new RuntimeException(e);
}
});Reason 6 – Ability to Modify Collections
A classic for loop can add or remove elements from the underlying list safely; doing so inside Stream.forEach() throws ConcurrentModificationException .
// for loop – works
for (int i = 0; i < ids.size(); i++) {
if (i < 1) {
ids.add(i);
}
}
System.out.println(ids);
// stream – fails
ids.stream().forEach(x -> {
if (x < 1) {
ids.add(x);
}
});Reason 7 – Better Debugging Experience
Step‑by‑step debugging of a traditional for loop is straightforward, while a lambda‑based Stream.forEach() hides the iteration logic, making breakpoints harder to place.
Reason 8 – Improved Readability
Procedural for code is often easier to understand at a glance than heavily chained functional streams, especially for developers unfamiliar with functional idioms.
Reason 9 – Simpler State Management
Maintaining mutable state across iterations (e.g., a flag) is trivial with a for loop; with streams you typically need thread‑safe wrappers like AtomicBoolean .
boolean flag = true;
for (int i = 0; i < 10; i++) {
if (flag) {
System.out.println(i);
flag = false;
}
}
// Stream version requires AtomicBoolean
AtomicBoolean flag1 = new AtomicBoolean(true);
IntStream.range(0,10).forEach(x -> {
if (flag1.get()) {
flag1.set(false);
System.out.println(x);
}
});Reason 10 – Direct Index Access
When you need to modify elements in place, a for loop lets you use the index directly; with streams you must create a new collection via map and collect the result.
for (int i = 0; i < ids.size(); i++) {
ids.set(i, ids.get(i) * 2);
}
ids = ids.stream().map(x -> x * 2).collect(Collectors.toList());Conclusion
The article presents ten technical arguments favoring the traditional for loop over Stream.forEach() for List traversal, while acknowledging that the best choice depends on the specific scenario. It also reminds readers to think critically about trends and choose the most appropriate tool for the job.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.