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.
On this page
🔁 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
forEachwith 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,forand 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
ConcurrentModificationExceptionwithmodCountand fix it withremoveIforIterator.remove() - Use labeled
breakandcontinueto 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.
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
forheader can declare several variables of the same type and run several updates separated by commas. All three parts are optional, sofor (;;)is a legal infinite loop. - For arrays, the generated code evaluates the array expression once and stores it in a hidden variable, then reads
lengthfrom that copy. Reassigning the array variable inside the loop doesn't change what you're iterating. - For an
Iterable, anullcollection throwsNullPointerExceptionat theiterator()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:
removeIf(predicate)(Java 8): one pass, no index juggling. OnArrayListit shifts the survivors once, so it's O(n) instead of O(n²) for repeatedremove.Iterator.remove(): when the decision needs more than a predicate. It removes the last element returned bynext()and keeps the counts in sync.- Iterate indexes backwards (
for (int i = list.size() - 1; i >= 0; i--)): works, but it's easy to get wrong. - Collect, then remove, or use
CopyOnWriteArrayListwhen 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:
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 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.
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.
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 labelexits outer loop;continue labelnext outer pass- while(false): compile error; int overflow, byte wrap, float step: infinite
- perf: int counters, no boxing, avoid LinkedList.get(i)