The enhanced for statement, usually called for-each, is the loop most Java code uses most often: for (Order o : orders) { ... }. It looks like it simply visits every element, and the exceptions are where production bugs live: a ConcurrentModificationException that appears only for some inputs, a loop over a List<Integer> that allocates more than you expected, a loop that silently skips an element after a removal, or a NullPointerException from a line with no visible dereference.
This article explains for-each from the specification up. It shows the two shapes the compiler turns the loop into, then uses that to explain mutation during iteration, performance, writing your own Iterable, and the edges of the language, including features added in Java 21 and one that was dropped. Every behaviour described here was checked by compiling and running small programs on JDK 23.
The loop is syntactic sugar for one of two loops
Section 14.14.2 of the Java Language Specification defines the enhanced for statement by translation. The compiler looks at the static type of the expression after the colon. If it is an array type, the loop becomes an indexed loop. If it is a subtype of java.lang.Iterable, the loop becomes an iterator loop. Anything else is a compile error, which is why you cannot write for-each over a Stream or an Iterator directly.
// What you write
for (String name : names) { System.out.println(name); }
// If names is a String[]: the array expression is evaluated ONCE into a hidden local
String[] a$ = names;
for (int i$ = 0; i$ < a$.length; i$++) {
String name = a$[i$];
System.out.println(name);
}
// If names is an Iterable<String> (any Collection, or your own class)
for (Iterator<String> it$ = names.iterator(); it$.hasNext(); ) {
String name = it$.next();
System.out.println(name);
}Three consequences follow directly. First, the expression is evaluated once: reassigning the array variable inside the loop does not change what is being iterated. A test that reassigns arr = new int[]{9} inside for (int x : arr) still prints 1, 2, 3. Second, if the expression is null, the hidden a$.length or names.iterator() call throws NullPointerException, which is why the stack trace points at the loop header. Third, the loop variable is a new local holding a copy of the element. Assigning to it changes nothing in the array or collection; for primitives that is obvious, and for objects it means you can mutate the object but not replace the slot.
A worked example: totalling orders three ways
Suppose an order service needs the total of line items above a threshold. The input arrives as an int[] of cents from a binary decoder in one path, and as a List<LineItem> in another.
record LineItem(String sku, int cents, int qty) {}
static long totalArray(int[] cents, int threshold) {
long total = 0;
for (int v : cents) { // indexed loop, no allocation, no boxing
if (v >= threshold) total += v;
}
return total;
}
static long totalItems(List<LineItem> items, int threshold) {
long total = 0;
for (var item : items) { // var infers LineItem (Java 10+)
long line = (long) item.cents() * item.qty();
if (line >= threshold) total += line;
}
return total;
}
static long totalNewestFirst(List<LineItem> items, int limit) {
long total = 0;
int seen = 0;
for (var item : items.reversed()) { // SequencedCollection view, Java 21+
if (seen++ == limit) break;
total += (long) item.cents() * item.qty();
}
return total;
}The array version compiles to a counted loop and is as cheap as a hand-written one. The list version calls items.iterator() once and hasNext/next per element. The third version uses List.reversed(), added by JEP 431 in Java 21, which returns a reverse-ordered view without copying, so for-each can walk backwards without index arithmetic. break and continue work exactly as in any other loop, and so do labels: outer: for (var row : grid) for (var cell : row) if (cell.isEmpty()) continue outer;.
Mutation during iteration: why ConcurrentModificationException is unreliable
The general-purpose collections in java.util (ArrayList, HashMap, LinkedList and friends) have fail-fast iterators. Each collection keeps a modification counter; the iterator records it when created and checks it in next(). A structural change made through the collection rather than the iterator makes the counts differ, and the next next() call throws. The Javadoc is explicit that this is best-effort and must not be relied on for correctness.
The best-effort part bites in a well-known way. In an ArrayList of a, b, c, removing "a" inside a for-each throws on the next step. Removing "b", the second-to-last element, does not throw: the size drops to 2, the cursor is already 2, hasNext() returns false, and the loop ends having never visited "c". We ran exactly that on JDK 23: the first case threw, the second printed [a, c] with no error. A loop that works in a unit test with three elements can therefore silently skip work in production.
// Wrong: may throw, may silently skip elements
for (String s : list) if (s.startsWith("tmp")) list.remove(s);
// Right: remove through the iterator that is doing the walking
for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
if (it.next().startsWith("tmp")) it.remove();
}
// Right and shorter (Java 8+): one pass, O(n) for ArrayList
list.removeIf(s -> s.startsWith("tmp"));
// Replacing values in place: ListIterator.set or replaceAll
list.replaceAll(String::strip);
// Maps: iterate entries, remove through the entry set's iterator or removeIf
map.entrySet().removeIf(e -> e.getValue().isExpired());Concurrent collections make different promises. CopyOnWriteArrayList iterators walk a snapshot taken when the loop began: they never throw, never see later writes, and their remove() is unsupported. ConcurrentHashMap iterators are weakly consistent: they never throw and may or may not reflect updates made during the walk. Neither makes a for-each over shared state atomic. If another thread is changing a plain ArrayList, for-each is a data race whether or not an exception appears, and the fix is synchronisation or a concurrent collection, not a try/catch around the loop. The ConcurrentHashMap deep dive covers those iteration guarantees in detail.
Performance: what costs, what does not
For arrays, for-each is free: the generated bytecode is the same counted loop you would write,. For collections there are three costs worth knowing.
- The iterator object. Each loop allocates one iterator. In hot code the JIT's escape analysis often removes that allocation when the iterator does not escape, but this depends on inlining and is not guaranteed. Measure before you rewrite.
- Boxing.
for (int v : List<Integer>)callsnext()to get anIntegerand then unboxes it. The unboxing is cheap; the real cost is that every element is a separate heap object, so iteration chases pointers through memory. If a hot path sums millions of numbers, store them in anint[]or a primitive collection, not aList<Integer>. - Access pattern. For
LinkedList, for-each is O(n) overall whilefor (int i...) list.get(i)is O(n^2), because eachgetwalks from an end. ForArrayListboth are O(n). For-each is the safe default when you do not know theListimplementation; theRandomAccessmarker interface exists to tell generic code when indexed access is cheap.
Do not trust intuition. Use JMH, which handles warm-up, dead-code elimination and forking for you:
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS)
@Fork(2) @Warmup(iterations = 5) @Measurement(iterations = 5)
public class LoopBench {
@Param({"1000", "1000000"}) int n;
int[] arr; List<Integer> boxed; List<Integer> linked;
@Setup public void setup() {
arr = new Random(42).ints(n).toArray();
boxed = Arrays.stream(arr).boxed().collect(Collectors.toCollection(ArrayList::new));
linked = new LinkedList<>(boxed);
}
@Benchmark public long array() { long s = 0; for (int v : arr) s += v; return s; }
@Benchmark public long boxedEach() { long s = 0; for (int v : boxed) s += v; return s; }
@Benchmark public long linkedEach() { long s = 0; for (int v : linked) s += v; return s; }
@Benchmark public long linkedGet() { long s = 0; for (int i = 0; i < linked.size(); i++) s += linked.get(i); return s; }
}Return the sum so the JIT cannot discard the loop. Run it on the JDK and hardware you deploy on; the ratios between these four change with heap layout, element count and CPU cache size, which is exactly why no figures are quoted here.
Writing your own Iterable
Any class that implements Iterable<T> works in for-each. That is a cheap way to give a domain type a natural loop without exposing its internals. The contract has three rules that hand-written iterators often break: iterator() must return a fresh iterator each call so two loops do not share state; hasNext() must be idempotent, with no side effects, because callers may call it repeatedly; and next() must throw NoSuchElementException when exhausted.
/** Half-open range [from, to) stepping by step; usable in for-each. */
public record Range(long from, long to, long step) implements Iterable<Long> {
public Range {
if (step <= 0) throw new IllegalArgumentException("step must be > 0");
}
@Override public Iterator<Long> iterator() {
return new Iterator<>() { // fresh state per loop
private long next = from;
@Override public boolean hasNext() { return next < to; } // no side effects
@Override public Long next() {
if (next >= to) throw new NoSuchElementException();
long v = next;
next += step;
return v;
}
};
}
}
for (long page : new Range(0, 10_000, 500)) fetchPage(page, 500);Two notes. This iterator boxes each long because Iterable is generic; for a hot numeric loop, a LongStream.range or a plain counted loop avoids that. And do not make a class implement both Iterable and Iterator by returning this from iterator(): the second for-each over the same object will see nothing, because the shared cursor is already exhausted. The Java generics guide explains why the element type has to be a reference type here.
for-each, Iterable.forEach and streams compared
| Need | for-each | Iterable.forEach(lambda) | Stream |
|---|---|---|---|
| break / continue / return from method | Yes | No (return only skips one element) | Only via short-circuit ops |
| Throw checked exceptions | Yes | No, must wrap | No, must wrap |
| Mutate local variables | Yes | No, effectively final only | No |
| Remove while walking | No; use Iterator or removeIf | No | No; filter into a new collection |
| Parallelism | Manual | No | parallel(), with care |
For-each is the right default for imperative work with side effects, early exit or checked exceptions. Streams win when the shape is filter, map, group, collect; the stream operations reference covers that side. Iterable.forEach is mostly useful for passing an existing method reference, as in names.forEach(System.out::println); on synchronized wrappers it holds the collection's lock for the whole walk, which for-each does not.
Language edges worth knowing
- Modifiers and var. The loop variable may be
finaland may usevarsince Java 10. Making itfinaldocuments that the body does not reassign it; see the var local type inference article for when inferred types hurt readability. - Maps are not Iterable. Loop over
map.entrySet()when you need keys and values; looping overkeySet()and callinggetdoes a second lookup per element. - Record patterns in the header were tried and removed. Java 20 previewed
for (Point(var x, var y) : points). JEP 440, which finalised record patterns in Java 21, removed that form. JDK 23 rejects it at compile time. Destructure inside the body, or use a pattern in aswitchorinstanceof. - Index needed? For-each hides the index. If you need it for output, keep a counter; if you need it to write back into an array or list, use a counted loop or
ListIterator. - Unboxing nulls.
for (int v : listOfInteger)throwsNullPointerExceptionon the first null element, because unboxing callsintValue(). UseInteger vif nulls are legal.
Failure modes and trade-offs
| Symptom | Cause | Fix |
|---|---|---|
| CME on some inputs, not others | Removing through the collection inside for-each | Iterator.remove, removeIf, or collect then remove |
| Element silently skipped, no exception | Removal of the second-to-last ArrayList element | Same fix; add a test with the target at each position |
| NPE at the loop header | Null array or collection | Return empty collections, never null |
| NPE with no null in sight | Unboxing a null Integer into int | Declare the loop variable as Integer |
| Second loop over an object sees nothing | iterator() returns a shared or exhausted instance | Return a fresh iterator per call |
| Sporadic wrong totals under load | Unsynchronised loop over a shared collection | Lock, copy, or use a concurrent collection |
| Hot loop slower than expected | Boxed elements, LinkedList, iterator not scalar-replaced | Measure with JMH; primitive arrays for numeric hot paths |
The trade-off is simple: for-each removes index bugs and makes the common case clear, at the cost of hiding the index and the iterator. When you need either, write the explicit loop. The collections framework overview explains which implementations are fail-fast, weakly consistent or snapshot-based.
What to do next
- Search your codebase for
.remove(calls inside for-each bodies and replace them withremoveIfor explicit iterators. - Add a unit test for each such loop that removes the second-to-last element, so a silent skip fails the build.
- Turn on the static-analysis checks your tools offer for modification during iteration and for
keySet()plusgetloops. - Audit APIs that can return a null collection and make them return empty ones instead.
- For one hot numeric loop, run the JMH harness above against your data size before changing representation.
- If you target Java 21 or later, replace reverse index loops over lists with
for (var x : list.reversed()).