The Hidden Cost of Iteration Choices

Every Java developer has written this:
public class IndexedForLoopSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
long sum = 0;
var n = data.size();
for (int i = 0; i < n; i++) {
sum += data.get(i);
}
return sum;
}
}It’s readable. Expressive. Safe. Clean. Easy to reason about.
But if list is a LinkedList, this innocent loop can take 3.8 seconds for 100,000 elements—while a different loop over the same data finishes in 0.15 milliseconds.
That’s not a micro-optimization. That’s a performance gap.
⚡ TL;DR (Quick Recap)
- Safest default: Enhanced for loop (works efficiently everywhere)
- Fastest on ArrayList: Indexed for, but only marginally
- Catastrophic: Indexed for on LinkedList (O(n²))
- Overhyped: parallelStream()—slower in all tested scenarios (for lightweight summation — not a universal rule)
Why Loop Strategy Still Matters
Modern Java abstracts a lot — Streams, lambdas, collections interfaces. But abstraction does not eliminate complexity.
The critical detail:
- ArrayList.get(i) → O(1)
- LinkedList.get(i) → O(n)
Now multiply that inside a loop. The JVM can optimize bytecode. It cannot fix a bad algorithm.
Benchmark Setup
It measures the average execution time per operation in nanoseconds, running in a shared benchmark state, with 2 forks (each doing 1 warmup fork), warming up for 5 iterations of 1 second each, and then measuring over 10 iterations of 1 second each.
Test parameters:
- JMH 1.37 on Java 25
- JVM: -Xms1g -Xmx1g -XX:+UseG1GC
All implementations follow a simple contract:
public interface SumStrategy {
long sumElements(List<Long> data);
}ArrayList: Performance Differences are Negligible
On ArrayList, everything works well.
At 10,000 elements:
- Indexed for: ~3,159 ns
- Enhanced for: ~3,181 ns
- Iterator: ~3,176 ns
- Stream: ~4,300 ns (~35% slower)
Differences are minimal because:
- Random access is constant time
- JIT optimizes loops aggressively
- Iterator overhead is mostly eliminated (Depends on JIT and context)
Choose readability. Performance differences are negligible.
Enhanced for vs Iterator: No Real Difference
This:
public class EnhancedForLoopSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
long sum = 0;
for (Long n : data) {
sum += n;
}
return sum;
}
}or this:
public class ExplicitIteratorSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
long sum = 0;
var it = data.iterator();
while (it.hasNext()) {
sum += it.next();
}
return sum;
}
}Same behavior. Same performance. Use whichever is more readable — typically the enhanced for.
LinkedList: Where Things Break Badly
Now the real story.
At 100,000 elements:
- Enhanced for: ~159,000 ns (0.15 ms)
- Indexed for: ~3,859,631,454 ns (3.8 s)
This is pure O(n²) behavior.
Why? Each get(i) walks the list from the start (or end).
So your loop becomes:
n * O(n) = O(n²)
This is not slow — it’s broken.
Streams: Clean, but Not Free
Streams improve readability, but they introduce overhead:
- mapToLong().sum() → avoids boxing, best stream option
- forEach() → slower due to lambda + mutable workaround
- parallelStream() → consistently slower (for lightweight summation — not a universal rule)
Why parallelStream fails here:
- Thread coordination overhead
- Small per-element workload
- Poor memory locality
Parallelism only helps when the work per element is expensive.
The Lambda “Mutable Capture” Problem
This pattern shows up often:
public class StreamForEachSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
long[] sum = {0};
data.stream().forEach(n -> sum[0] += n);
return sum[0];
}
}It works because lambdas require effectively-final variables.
But:
- Adds indirection
- Hurts readability
- Signals wrong abstraction
Prefer:
public class StreamReduceSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
return data.stream().reduce(0L, Long::sum);
}
}Array Conversion: Smart Idea, Wrong Context
public class ArrayConversionSum implements SumStrategy {
@Override
public long sumElements(List<Long> data) {
long[] arr = data.stream().mapToLong(Long::longValue).toArray();
long sum = 0;
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
}Then iterate.
In theory:
- No boxing
- Better CPU optimization
In practice:
- Conversion cost dominates
- 3–4× slower for single-pass operations
Only useful if:
- You reuse the array multiple times
- Heavy computation follows
Final Takeaways
Looping in Java feels trivial — but it’s one of the easiest places to introduce serious performance bugs.
The biggest lessons:
- Data structure defines performance — not loop syntax
- Enhanced for is the safest default
- Indexed loops can be dangerous in the wrong context
- Streams trade performance for clarity
- Parallelism is not a free optimization
This isn’t about shaving nanoseconds. It’s about avoiding seconds of latency from a single line of code.
You can find all the code on GitHub.
Originally posted on marconak-matej.medium.com.