You have used the for-each loop since the very first collections lesson: for (String f : fruits) walks every element with no index to manage. It is the safe default, and it hides something important. Underneath, for-each is not magic — it is shorthand for a small machine called an iterator that steps through the collection one element at a time. Most days you never need to know that.
The day you try to remove an element while you loop, it suddenly matters a great deal, because that is when an unmodified for-each explodes with a ConcurrentModificationException. The name says concurrent, but the trap springs even in a single thread — it has nothing to do with parallelism. This lesson makes the hidden machinery visible: how iteration actually works, why editing during a loop is dangerous, and the two clean ways to do it safely.
For-each is an iterator in disguise
Read the colon in for (String f : fruits) as in — for each string f in fruits. The loop runs the body once per element, handing you each in turn, and it works identically over a List, a Set, or an array because all of them are iterable.
When the compiler meets a for-each over a collection, it rewrites it. The rewrite calls fruits.iterator() to obtain an Iterator, then loops while that iterator still has a next element, calling next() to fetch each one. You never see those calls, but they happen on every pass — and that hidden iterator is what tracks how far through the collection you have read. It is also the very thing that blows up if the collection changes underneath it.
flowchart LR FE["for each s in list"] -->|"compiles to"| IT["obtain an Iterator"] IT --> LOOP["while hasNext()"] LOOP --> NX["next() returns one element"] NX --> LOOP style FE fill:#3776ab,color:#fff style IT fill:#c2410c,color:#fff
The indexed loop, and the Iterator up close
Before for-each, you walked a list by counting: for (int i = 0; i < list.size(); i++) { ... list.get(i) ... }. The index i is entirely in your hands, which is power and danger in equal measure — get the bound wrong and you read past the end, or off-by-one your way into skipping the first or last element. For-each removes that footgun by managing the position for you.
When you need finer control you drop down to an Iterator directly. It exposes two methods for reading: hasNext() asks is there another element?, and next() fetches it and advances. A while (it.hasNext()) { String s = it.next(); } loop is exactly what for-each compiles to, written out by hand. The reason to write it yourself is a third method iterators offer: remove().
flowchart LR S["start"] --> H["hasNext()?"] H -->|"yes"| N["next() gives the next element"] N --> H H -->|"no"| D["loop ends"] style H fill:#c2410c,color:#fff style N fill:#3776ab,color:#fff
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.add(30);
int sum = 0;
for (int n : nums) {
sum += n;
}
System.out.println("sum: " + sum);
}
}
The trap: editing while you iterate
Here is the surprise that bites everyone once. You loop over a list with for-each, and inside the loop you decide some elements should go: if (s.length() == 0) { list.remove(s); }. The program compiles. It runs. And partway through, it crashes with a ConcurrentModificationException.
The reason is that hidden iterator. It keeps track of how many elements it has handed you, and the collection keeps a count of how many times it has been structurally changed. When next() runs, the two are compared; if the collection changed outside the iterator — by your direct list.remove — the counts disagree and the iterator refuses to continue, because it can no longer guarantee it will not skip or repeat an element. Throwing is its way of failing loudly instead of silently corrupting your loop.
flowchart TD LOOP["removing during a for-each"] --> Q["who performs the remove?"] Q -->|"list.remove()"| BAD["ConcurrentModificationException"] Q -->|"it.remove()"| GOOD["safe, counts stay in sync"] Q -->|"collect, remove after loop"| GOOD2["safe, no exception"] style BAD fill:#b45309,color:#fff style GOOD fill:#3776ab,color:#fff style GOOD2 fill:#3776ab,color:#fff
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
nums.add(3);
Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
int n = it.next();
if (n == 2) {
it.remove(); // safe: removes through the iterator
}
}
for (int n : nums) {
System.out.println(n);
}
}
}
For-each over a list. The loop adds every element to a running total. Predict the exact output (one line).
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.add(30);
int sum = 0;
for (int n : nums) {
sum += n;
}
System.out.println("sum: " + sum);
}
}
The for-each visits 10, then 20, then 30, adding each to
sum.10 + 20 + 30 is
60.
When the compiler meets for (String s : list) over a collection, what does it rewrite the loop to use?
For-each over a collection is shorthand for obtaining an Iterator and looping while hasNext() is true, calling next() each pass. An indexed loop is a different, hand-managed form — for-each never generates one automatically for a collection.
Inside for (String s : list) { ... }, you call list.remove(s). What happens?
Removing through the collection directly, while a for-each (an iterator) is mid-flight, makes the change counts disagree. On the next call to next(), the iterator detects the outside change and throws ConcurrentModificationException — even in a single-threaded program.
Safe removal through the iterator. The value 2 is removed via it.remove(), then the list is printed. Predict the exact output (two lines).
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(1);
nums.add(2);
nums.add(3);
Iterator<Integer> it = nums.iterator();
while (it.hasNext()) {
int n = it.next();
if (n == 2) {
it.remove();
}
}
for (int n : nums) {
System.out.println(n);
}
}
}
it.remove()deletes the element most recently returned bynext()— here the2.Removing through the iterator keeps the counts in sync, so no exception is thrown; the list left is 1, 3.
An indexed loop. The position i is printed alongside each element. Predict the exact output (three lines).
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("cat");
words.add("dog");
words.add("bird");
for (int i = 0; i < words.size(); i++) {
System.out.println(i + ": " + words.get(i));
}
}
}
The index starts at 0 and stops before
size()(which is 3), so it takes the values 0, 1, 2.iis concatenated with the element, giving0: cat,1: dog,2: bird.
Recap
- For-each (
for (T x : coll)) is shorthand for stepping through a collection with an Iterator. - An
IteratoroffershasNext(),next(), andremove()— write the loop yourself when you need that control. - Calling the collection's own
remove()inside a for-each throwsConcurrentModificationException, because the iterator detects the outside change. - Remove safely via the iterator's
remove(), or by collecting items and removing them after the loop. - The indexed
forloop still works when you genuinely need a position, but for-each avoids the off-by-one errors an index invites.
That closes the core of working with collections. Next you will give your own types a natural sort order with Comparable and Comparator.