Course contents

Loops, for-each, break, continue and labels

while, do-while, for and for-each, what the enhanced for really compiles to, why removing in a loop throws ConcurrentModificationException, and how labels tame nested loops.

FreeLesson9 min readCoreReviewed Oct 2026 · Java 21
On this page
  1. Picture it first: for-each is a disguise
  2. Under the hood: loop shapes and the for-each translation
  3. Real world: break, continue and labels
  4. Advanced: infinite loops, reachability and performance
  5. How to say it in the interview

🔁 What are Java's loops?

A loop repeats a block while a condition holds. Java gives you four shapes: while (check first), do-while (run once, then check), the classic for (init, condition, update in one line) and the enhanced for (for (String s : names)), which walks an array or any Iterable without an index. break, continue and labels let you leave or skip iterations early, even across nested loops.

For this lesson, we'll cover each loop shape, open up what the compiler turns the enhanced for into, and use that to explain the ConcurrentModificationException that every Java developer meets sooner or later. Then we'll handle labeled break and continue, infinite loops, and a few performance notes where the JIT does more than people expect.

Below the line (out of scope)

  • Streams and forEach with lambdas get their own lesson in the collections and functional modules
  • Concurrent collections in depth belong to the multithreading module

The 30-second answer

while checks before each pass, do-while runs at least once, and for bundles init, condition and update. The enhanced for is syntactic sugar: over an array it becomes an indexed loop on a copied reference; over an Iterable it becomes an Iterator with hasNext()/next(). That's why removing from an ArrayList inside a for-each throws ConcurrentModificationException: the iterator's next() sees modCount changed. Use Iterator.remove() or removeIf. Unlabeled break/continue affect the innermost loop; a label like outer: lets you exit or continue the outer one.

What you’ll learn

  • Choose between while, do-while, for and the enhanced for, and explain how each checks its condition
  • Describe the for-each desugaring for arrays and for Iterable (JLS 14.14.2)
  • Explain ConcurrentModificationException with modCount and fix it with removeIf or Iterator.remove()
  • Use labeled break and continue to control nested loops, and know when to extract a method instead
  • Spot hidden infinite loops and the loop habits that matter for JIT performance

01Picture it first: for-each is a disguise

The enhanced for is a costume. Underneath, it's one of two ordinary loops, and which one depends on what you're iterating. The JLS (§14.14.2) spells out the translation exactly.

Fig. 1

The enhanced for loop desugars to an indexed loop over a copied array reference for arrays, and to an Iterator with hasNext and next for any Iterable

The enhanced for loop desugars to an indexed loop over a copied array reference for arrays, and to an Iterator with hasNext and next for any Iterable

Two consequences fall straight out of this picture. First, the loop variable is a copy: assigning to it (v *= 10) changes nothing in the array or list. Second, for collections, every step calls next() on an iterator, and that's where the collection gets its chance to complain if you changed it behind the iterator's back.

02Under the hood: loop shapes and the for-each translation

The four shapes are mostly a matter of where the condition sits:

import java.util.Iterator;
import java.util.List;

public class LoopShapes {
    public static void main(String[] args) {
        int i = 0;
        while (i < 3) { i++; }                     // may run zero times

        int j = 10;
        do { j++; } while (j < 3);                 // runs once: j is 11

        for (int a = 0, b = 10; a < b; a += 3, b -= 3) {
            System.out.print(a + ":" + b + " ");   // 0:10 3:7
        }
        System.out.println();

        int[] nums = {1, 2, 3};
        for (int v : nums) { v *= 10; }            // nums is still [1, 2, 3]

        // What the compiler generates for: for (String s : names) { ... }
        List<String> names = List.of("ann", "bob");
        for (Iterator<String> it = names.iterator(); it.hasNext(); ) {
            String s = it.next();
            System.out.println(s);
        }
    }
}

Notice three things:

  • The for header can declare several variables of the same type and run several updates separated by commas. All three parts are optional, so for (;;) is a legal infinite loop.
  • For arrays, the generated code evaluates the array expression once and stores it in a hidden variable, then reads length from that copy. Reassigning the array variable inside the loop doesn't change what you're iterating.
  • For an Iterable, a null collection throws NullPointerException at the iterator() call, before the first pass.

Now the exception.

Try it yourself first

Before reading on, predict both outputs. One of them is a famous surprise.

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.List;

public class RemoveWhileLooping {
    public static void main(String[] args) {
        List<String> a = new ArrayList<>(List.of("a", "b", "c"));
        try {
            for (String s : a) {
                if (s.equals("a")) a.remove(s);
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("CME");                        // printed
        }

        List<String> b = new ArrayList<>(List.of("a", "b", "c"));
        for (String s : b) {
            if (s.equals("b")) b.remove(s);                    // second-to-last
        }
        System.out.println(b);                                 // [a, c], no exception!

        List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
        nums.removeIf(n -> n % 2 == 0);                        // [1, 3, 5]
        for (Iterator<Integer> it = nums.iterator(); it.hasNext(); ) {
            if (it.next() == 3) it.remove();                   // safe removal
        }
        System.out.println(nums);                              // [1, 5]
    }
}

Here's the mechanism. ArrayList keeps a modCount field that increments on every structural change. The iterator copies it into expectedModCount when it's created, and next() throws if the two differ. That's a fail-fast check, not a guarantee: the javadoc says it's on a best-effort basis.

The second case is the surprise. After removing "b", the list has size 2 and the iterator's cursor is already 2. hasNext() is cursor != size, so it returns false, next() is never called, and the loop ends quietly. No exception, and "c" is never visited. A bug that only throws sometimes is worse than one that always throws.

The fixes, in the order I'd reach for them:

  1. removeIf(predicate) (Java 8): one pass, no index juggling. On ArrayList it shifts the survivors once, so it's O(n) instead of O(n²) for repeated remove.
  2. Iterator.remove(): when the decision needs more than a predicate. It removes the last element returned by next() and keeps the counts in sync.
  3. Iterate indexes backwards (for (int i = list.size() - 1; i >= 0; i--)): works, but it's easy to get wrong.
  4. Collect, then remove, or use CopyOnWriteArrayList when the list is read far more often than written. Its iterators work on a snapshot and never throw.

03Real world: break, continue and labels

break leaves the innermost loop (or switch). continue skips to the next iteration of the innermost loop. When you have nested loops and need to leave both, you label the outer one:

Fig. 2

A labeled break exits both loops at once, while a plain break only leaves the inner loop and continue outer jumps to the next row

A labeled break exits both loops at once, while a plain break only leaves the inner loop and continue outer jumps to the next row
public class GridSearch {
    record Cell(int row, int col) {}

    static Cell findNegative(int[][] grid) {
        Cell found = null;
        search:
        for (int r = 0; r < grid.length; r++) {
            for (int c = 0; c < grid[r].length; c++) {
                if (grid[r][c] < 0) {
                    found = new Cell(r, c);
                    break search;                 // leaves BOTH loops
                }
            }
        }
        return found;
    }

    public static void main(String[] args) {
        int[][] grid = {{1, 2, 3}, {4, -1, 6}, {7, 8, 9}};
        System.out.println(findNegative(grid));   // Cell[row=1, col=1]

        outer:
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (j == 1) continue outer;       // skip the rest of this row
                System.out.print(i + "" + j + " ");
            }
        }
        System.out.println();                     // 00 10 20
    }
}
In the real world

In production code I mostly see labels in parsers, grid and matrix searches, and retry loops that wrap a switch (where a plain break would only leave the switch). The usual review comment is the same each time: if the nested loop has its own name in your head, extract it into a method and use return. That reads better than a label and is easier to test. Labels are fine when extraction would mean passing five local variables around.

A few facts interviewers like: a label can sit on any statement, including a plain block, so block: { if (done) break block; ... } is legal. continue with a label must target a loop. And Java has no goto: it's a reserved keyword that does nothing, and labels are the structured replacement.

04Advanced: infinite loops, reachability and performance

The compiler tracks reachability (JLS 14.22) and has opinions about loops. while (true) { } with no break means the statement after it is unreachable, which is a compile error, and a method ending in such a loop needs no return. while (false) { ... } doesn't compile at all: the body is unreachable. Oddly, if (false) { ... } is allowed, on purpose, so you can switch debug code off with a constant.

Some infinite loops hide in plain sight:

Loop Why it never ends
for (int i = 0; i <= Integer.MAX_VALUE; i++) i overflows to MIN_VALUE, always <=
for (byte b = 0; b < 200; b++) a byte tops out at 127, then wraps to -128
for (float f = 0; f != 1.0f; f += 0.1f) 0.1f isn't exact, so f skips past 1.0f
for (int x = 0; x < 5; x++); { ... } the stray ; is the loop body; the block runs once

The last one isn't infinite, but it belongs in the same family: the loop spins five times doing nothing, then the block runs once.

Where candidates slip

Many candidates are quick to hand-optimise loops: caching list.size() in a local, counting down instead of up, unrolling by hand. The JIT already hoists loop-invariant reads, eliminates array bounds checks in simple int-indexed loops, and unrolls hot loops. Hand tricks mostly make code harder to read. The changes that really matter are the ones the JIT can't do for you: not calling get(i) on a LinkedList (O(n) each, O(n²) total), and not boxing.

Performance angle

Boxing is the classic loop tax. Long sum = 0L; for (long i = 0; i < 1_000_000; i++) sum += i; unboxes and allocates a new Long on almost every iteration, about a million short-lived objects, while a primitive long sum allocates nothing. Escape analysis sometimes rescues you, but don't count on it. Also prefer int loop counters: HotSpot recognises counted loops with int induction variables and applies its strongest optimisations to them (range-check elimination, unrolling, vectorisation). Long-running loops are also where the JVM switches from interpreter to compiled code mid-loop, a trick called on-stack replacement, which is why the first few thousand iterations of a microbenchmark are slower than the rest.

We look at boxing costs in more detail in Wrapper classes and autoboxing, and at the branching side of control flow in if, switch and the switch expression.

05How to say it in the interview

When I ask about loops, I'm listening for three things: do you know the enhanced for is sugar over an Iterator, can you explain ConcurrentModificationException with modCount instead of "you can't modify a list while looping", and do you reach for removeIf or Iterator.remove() without being prompted. If you mention the second-to-last-element quirk, I know you've looked at the source.

In the interview

"The enhanced for compiles to an indexed loop for arrays and to an Iterator loop for any Iterable. ArrayList's iterator is fail-fast: it compares the list's modCount with the count it saw at creation, so removing through the list inside a for-each throws ConcurrentModificationException on the next next(). The fix is removeIf or Iterator.remove(). For nested loops I use a labeled break or, more often, extract a method and return. And I keep loop counters primitive ints, because boxing in a hot loop allocates on every iteration."

Here's how this might look on the whiteboard:

On the whiteboard

Loops

  • while: 0+ passes; do-while: 1+; for: init; cond; update
  • for-each over array: indexed loop on a copied ref
  • for-each over Iterable: iterator(), hasNext(), next()
  • loop var is a copy: assigning it changes nothing
  • remove in for-each: CME via modCount (or silent skip at size-1)
  • fix: removeIf, Iterator.remove(), CopyOnWriteArrayList
  • break label exits outer loop; continue label next outer pass
  • while(false): compile error; int overflow, byte wrap, float step: infinite
  • perf: int counters, no boxing, avoid LinkedList.get(i)
Practise this topicOperators & control flow: interview questions & scenarios
Further reading (4)