🗃️ Collections Framework · Intermediate

Iterator & ConcurrentModificationException in Java

Fail-fast iterators; removing safely with iterator.remove or removeIf.

🧩 The mysteryRemove an element while looping and Java throws ConcurrentModificationException, even with no threads in sight. Stranger still: remove the right element and it doesn't throw at all.

for-each has a hidden iterator

for (String s : list) is shorthand for an **Iterator**: hasNext() / next() calls. Most iterators are fail-fast: they remember the list's modification count, and if the list changes behind their back, the **next next() throws ConcurrentModificationException**.

for (String s : list) { ... }
// is really:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    String s = it.next(); // checks here
}
🔮 Predict it

Your turn

What happens?

var list = new ArrayList<String>();
list.addAll(List.of("a", "b", "c", "d"));
for (String s : list) {
    if (s.equals("a")) {
        list.remove(s);
    }
}
System.out.println(list);
  1. Prints [b, c, d]
  2. Throws ConcurrentModificationException
  3. Prints [a, b, c, d]
Show the answer

list.remove(s) changes the list while the hidden iterator is active. On the very next next(), the iterator notices the count changed and throws. No threads needed.

Remove through the iterator

**it.remove() deletes the element last returned by next()** and keeps the iterator's bookkeeping in sync. No exception.

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().isBlank()) {
        it.remove();   // safe
    }
}

Even simpler: removeIf

**removeIf(predicate) removes every matching element in one safe pass**. It's the cleanest way to filter a collection in place.

var nums = new ArrayList<>(List.of(1, 2, 3, 4));
nums.removeIf(n -> n % 2 == 0);
// [1, 3]
🔮 Predict it

The sneaky one

Now we remove the second-to-last element. What happens?

var list = new ArrayList<String>();
list.addAll(List.of("a", "b", "c", "d"));
for (String s : list) {
    if (s.equals("c")) {
        list.remove(s);
    }
}
System.out.println(list);
  1. Throws ConcurrentModificationException
  2. [a, b, d]
  3. [a, b, c, d]
Show the answer

No exception! After removing "c" the size drops to 3. The cursor is at 3 too, so **hasNext() returns false and the loop ends before next() can check**. "d" is silently never visited.

⚠️ The trap

Fail-fast is best effort

As you just saw, the check isn't a guarantee. Never rely on the exception to catch your mistake: code that "works" in tests may be silently skipping elements. Always modify through it.remove() or removeIf.

💼 In the real world

On the job

ConcurrentModificationException is one of the most common Java exceptions in single-threaded code. When collections really are shared between threads, use concurrent ones like ConcurrentHashMap or CopyOnWriteArrayList, whose iterators never throw it.

Key takeaways

  1. Don't call list.remove(...) inside a for-each over that list
  2. iterator.remove() removes the element last returned by next()
  3. removeIf(predicate) is the cleanest way to filter in place
  4. Fail-fast is best-effort, not a guarantee — never rely on it
🤯 Did you know?

The fail-fast check is a plain int counter named modCount, declared in AbstractList. Every structural change bumps it, and each iterator compares it with the value it saw at the start.

Practice questions

What happens when this runs?

var list = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : list) {
    if (n == 2) {
        list.remove(n);
    }
}
System.out.println(list);
  1. [1, 3, 4]
  2. Throws ConcurrentModificationException
  3. [1, 2, 3, 4]
  4. [1, 4]
Check your answer

Throws ConcurrentModificationException. list.remove(n) (n is an Integer, so it removes by value) changes the list while the hidden iterator is active. The following next() notices and throws.

Fill the blank to remove blank names safely during iteration.

Iterator<String> it = names.iterator();
while (it.hasNext()) {
    if (it.next().isBlank()) {
        ___;
    }
}
  1. names.remove(it.next())
  2. it.remove()
  3. names.remove(it)
  4. it.next().remove()
Check your answer

it.remove(). iterator.remove() deletes the element most recently returned by next() and keeps the iterator's bookkeeping in sync. Calling names.remove(...) would trigger ConcurrentModificationException.

Next: how does Java know that "apple" comes before "banana", and how do you sort people by city, then name, in one line?