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.

Advertisement

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);
}
What javac does with for (T x : expr) -- JLS 14.14.2for (T x : expr) bodysource you writestatic type of expr?decided at compile timearray typeIterable subtypeIndexed loopT[] #a = expr; (evaluated once)for (int #i = 0; #i < #a.length; #i++)Iterator loopIterator<T> #it = expr.iterator();while (#it.hasNext()) x = #it.next();No allocation, no CMEarray length fixed; null array -> NPECollection decides semanticsfail-fast, weakly consistent or snapshotIn both forms the loop variable is a fresh local per iteration; the index or iterator is hidden from you.Everything surprising about for-each comes from which branch you are on and what the collection's iterator promises.
Figure 1. javac picks one of two translations from the static type of the expression. Arrays become a counted loop over a copy of the array reference; Iterables become a hasNext/next loop, so the collection's iterator decides what happens under concurrent change.

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;.

Advertisement

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>) calls next() to get an Integer and 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 an int[] or a primitive collection, not a List<Integer>.
  • Access pattern. For LinkedList, for-each is O(n) overall while for (int i...) list.get(i) is O(n^2), because each get walks from an end. For ArrayList both are O(n). For-each is the safe default when you do not know the List implementation; the RandomAccess marker 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

Needfor-eachIterable.forEach(lambda)Stream
break / continue / return from methodYesNo (return only skips one element)Only via short-circuit ops
Throw checked exceptionsYesNo, must wrapNo, must wrap
Mutate local variablesYesNo, effectively final onlyNo
Remove while walkingNo; use Iterator or removeIfNoNo; filter into a new collection
ParallelismManualNoparallel(), 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 final and may use var since Java 10. Making it final documents 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 over keySet() and calling get does 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 a switch or instanceof.
  • 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) throws NullPointerException on the first null element, because unboxing calls intValue(). Use Integer v if nulls are legal.

Failure modes and trade-offs

SymptomCauseFix
CME on some inputs, not othersRemoving through the collection inside for-eachIterator.remove, removeIf, or collect then remove
Element silently skipped, no exceptionRemoval of the second-to-last ArrayList elementSame fix; add a test with the target at each position
NPE at the loop headerNull array or collectionReturn empty collections, never null
NPE with no null in sightUnboxing a null Integer into intDeclare the loop variable as Integer
Second loop over an object sees nothingiterator() returns a shared or exhausted instanceReturn a fresh iterator per call
Sporadic wrong totals under loadUnsynchronised loop over a shared collectionLock, copy, or use a concurrent collection
Hot loop slower than expectedBoxed elements, LinkedList, iterator not scalar-replacedMeasure 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

  1. Search your codebase for .remove( calls inside for-each bodies and replace them with removeIf or explicit iterators.
  2. Add a unit test for each such loop that removes the second-to-last element, so a silent skip fails the build.
  3. Turn on the static-analysis checks your tools offer for modification during iteration and for keySet() plus get loops.
  4. Audit APIs that can return a null collection and make them return empty ones instead.
  5. For one hot numeric loop, run the JMH harness above against your data size before changing representation.
  6. If you target Java 21 or later, replace reverse index loops over lists with for (var x : list.reversed()).
Key takeaway: The enhanced for statement is compiler sugar for one of two loops: a counted loop over a once-evaluated array, or a hasNext/next loop over an Iterable's iterator. Almost every surprise follows from that. Arrays cost nothing and cannot throw ConcurrentModificationException. Collections inherit their iterator's promises, which are fail-fast on a best-effort basis and can silently skip an element. Remove through the iterator or with removeIf, avoid boxed numeric hot paths, return fresh iterators from your own Iterables, and measure with JMH rather than guessing.