Made by VeraStack Labs
Crossing names off a register
Do not change the list while you read it.
A teacher reads the class register aloud: "Asha, Ravi, Meera, Kiran." You are crossing out the names of students who have left. Suppose that halfway through, you rub out Ravi's name without telling the teacher. The teacher is now pointing at a row that no longer holds what they expect. They stop and say, "Hold on, the register changed."
Java behaves the same way. Walking through a collection from the first item to the last is called iterating, and the loop that does it is reading the collection as it goes. If you add or remove items from inside that loop, the collection and the loop disagree about where they are. Java would rather stop loudly than carry on with wrong answers, so it throws a ConcurrentModificationException. That is a long name for "the collection changed while you were reading it."
This lesson shows the three safe ways to walk a collection, and the one safe way to remove things while you do.
Remember: read freely, but change the list only through the tool that is doing the reading, or after the loop is over.
An iterator keeps score
It writes down a version and checks it each step.
Every for (x : list) loop in Java uses a helper behind the scenes called an iterator. An iterator is a small object with a bookmark. It knows which item comes next, and it has two jobs: hasNext() answers "is there another item?", and next() hands over that item and moves the bookmark along.
Most collections also keep a version number that goes up every time you add or remove an item. When an iterator is created, it writes down the version it started with. On each step it compares: is the list still on the version I remember? If a removal happened behind its back, the numbers differ, and the iterator throws.
This design is called fail-fast, meaning it fails the moment it notices something is wrong rather than wandering on with bad data. Java describes it as a best effort, not a promise, so never write code that relies on the exception turning up. Treat it as a bug alarm.
Three ways to loop
for-each, Iterator, and removeIf.
Every block below can be pasted into jshell. Start with the loop you already know.
List<String> register = List.of("Asha", "Ravi", "Meera");
for (String name : register) {
System.out.println(name);
}Paste into jshell to try it
Asha
Ravi
MeeraThe for-each loop is the neatest way to read every item. It works on any collection. Use it whenever you only need to look.
Now the same walk written out by hand, which is what the compiler turns for-each into:
List<String> register = List.of("Asha", "Ravi", "Meera");
Iterator<String> it = register.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}Paste into jshell to try it
Asha
Ravi
MeeraNow the trap. Here we cross out every name that ends in "a" while looping over the list:
List<String> register = new ArrayList<>(List.of("Asha", "Ravi", "Meera", "Kiran"));
try {
for (String name : register) {
if (name.endsWith("a")) {
register.remove(name); // the loop is still reading the list
}
}
} catch (ConcurrentModificationException e) {
System.out.println("Stopped by " + e.getClass().getSimpleName());
}Paste into jshell to try it
Stopped by ConcurrentModificationExceptionregister.remove(name) changed the list, but the loop's iterator did not know. On the next step it saw a different version and stopped. We caught the exception here only so you can see it. In real code, a crash like this is a bug to fix, not to catch.
The fix is to let the iterator do the removing. It has a remove() method that deletes the item next() just returned, and it updates its own copy of the version, so it never gets confused.
List<String> register = new ArrayList<>(List.of("Asha", "Ravi", "Meera", "Kiran"));
Iterator<String> it = register.iterator();
while (it.hasNext()) {
String name = it.next();
if (name.endsWith("a")) {
it.remove(); // the iterator removes it, so it knows the list changed
}
}
System.out.println(register);Paste into jshell to try it
[Ravi, Kiran]Call remove() once after each next(), never twice in a row. Calling it before the first next(), or twice without a next() in between, throws an IllegalStateException.
The shortest fix is a method that does the whole loop for you. removeIf takes a rule and removes every item that matches it.
List<String> register = new ArrayList<>(List.of("Asha", "Ravi", "Meera", "Kiran"));
register.removeIf(name -> name.endsWith("a"));
System.out.println(register);Paste into jshell to try it
[Ravi, Kiran]The rule name -> name.endsWith("a") is a lambda, which is a tiny function written inline: "given a name, answer true if it ends in a." You will write more of these in later lessons. Prefer removeIf when the job is simply "remove everything that matches".
One more tool exists for lists: a ListIterator. It can also set (replace) the item it just returned and add a new one beside it, again without upsetting anything. Problem 3 uses it.
Remember: to remove while looping, use Iterator.remove(). To remove everything that matches a rule, use removeIf.
Build the alarm yourself
A tiny list with a version number.
To see why the exception happens, here is a small list that has the version number and the check. It is a cut-down ArrayList: an array, a size, and a version that goes up on every change.
static class TinyList<T> implements Iterable<T> {
private Object[] items = new Object[4];
private int size = 0;
private int version = 0; // goes up on every add or remove
void add(T value) {
if (size == items.length) {
items = Arrays.copyOf(items, size * 2);
}
items[size++] = value;
version++;
}
void removeAt(int index) {
System.arraycopy(items, index + 1, items, index, size - index - 1);
items[--size] = null;
version++;
}
@Override
public Iterator<T> iterator() {
return new Iterator<>() {
private int position = 0;
// The iterator writes down the version it started with.
private final int expectedVersion = version;
@Override
public boolean hasNext() {
return position < size;
}
@Override
@SuppressWarnings("unchecked")
public T next() {
if (version != expectedVersion) {
throw new ConcurrentModificationException();
}
if (position >= size) {
throw new NoSuchElementException();
}
return (T) items[position++];
}
};
}
}Paste into jshell to try it
Go through it in order.
versionstarts at 0 and goes up by one inaddand inremoveAt. That is how the list notices it has changed.iterator()returns an anonymous class, meaning an object made on the spot without giving its class a name. It holdsposition, the bookmark, andexpectedVersion, the version it wrote down at birth.next()checks first. Ifversionhas moved on, it throwsConcurrentModificationException. Only then does it hand back the item.- The
@SuppressWarnings("unchecked")is there because the array holds plainObjects, and we know they are reallyTs.
Now remove from the list inside a for-each loop over it:
TinyList<String> register = new TinyList<>();
register.add("Asha");
register.add("Ravi");
register.add("Meera");
try {
for (String name : register) {
System.out.println("saw " + name);
register.removeAt(0); // the version changes mid-loop
}
} catch (ConcurrentModificationException e) {
System.out.println("Stopped by " + e.getClass().getSimpleName());
}Paste into jshell to try it
saw Asha
Stopped by ConcurrentModificationExceptionThe first step worked and printed "Asha". We then removed an item, so version moved on. The second next() compared the numbers and threw. That is the whole mechanism. The real ArrayList keeps the same kind of counter, and calls it modCount.
What each walk costs
n is the number of items in the list.
| Operation | Time | Why |
|---|---|---|
Walk the whole list (for-each, Iterator) | O(n) | Each item is visited once |
iterator.remove() on an ArrayList | O(n) | Items after the gap shift left |
iterator.remove() on a LinkedList | O(1) | Only a few links change |
Remove many items one by one with iterator.remove() on an ArrayList | O(n) per item | Each removal shifts the tail again |
removeIf on an ArrayList | O(n) | The usual implementation closes the gaps in a single pass |
list.remove(x) inside a loop | O(n) per call, and unsafe | It searches, shifts, and breaks the iterator |
Two things to take from the table. First, a loop is always O(n), and what you do inside it decides whether the total grows much faster. Second, if you have a lot to remove from an ArrayList, removeIf is the kind choice, because one pass beats many shifts.
Three problems, safe loops
Remove, deduplicate, and replace in place.
Problem 1
Remove the short names
Write removeShort, which removes every name shorter than minLength from the list, in place. For ["Al", "Asha", "Bo", "Meera"] and minLength = 3, the list becomes ["Asha", "Meera"]. The order of the rest stays the same.
Start here
static void removeShort(List<String> names, int minLength) {
throw new UnsupportedOperationException("remove every name shorter than minLength, in place");
}Paste into jshell to try it
Stuck?
Show a solution
static void removeShort(List<String> names, int minLength) {
Iterator<String> it = names.iterator();
while (it.hasNext()) {
if (it.next().length() < minLength) {
it.remove();
}
}
}Paste into jshell to try it
The iterator is the only thing allowed to remove during the walk, because it updates its own version number. The next() call is made once per step, and remove() follows it directly. names.removeIf(name -> name.length() < minLength) does the same job in one line, and is the better choice in everyday code. The iterator version is what an interview wants to see. Both take O(n) steps, plus the shifting cost of an ArrayList.
Problem 2
Drop the repeats
Write dropRepeats, which takes a sorted list of numbers and removes every number that equals the one before it, in place. For [1, 1, 2, 3, 3, 3, 4] the list becomes [1, 2, 3, 4]. An empty list stays empty.
Start here
static void dropRepeats(List<Integer> sorted) {
throw new UnsupportedOperationException("remove each number that equals the one before it");
}Paste into jshell to try it
Stuck?
Show a solution
static void dropRepeats(List<Integer> sorted) {
Iterator<Integer> it = sorted.iterator();
Integer previous = null;
while (it.hasNext()) {
Integer current = it.next();
// equals, not ==: two Integer objects can hold the same number
if (current.equals(previous)) {
it.remove();
} else {
previous = current;
}
}
}Paste into jshell to try it
Because the list is sorted, any repeats are neighbours, so one previous variable is enough and no extra collection is needed. When a number is removed, previous stays the same, so a run like 3, 3, 3 loses its second and third. equals matters: == on two Integer objects compares whether they are the same object, which can be false for equal numbers above 127. One pass gives O(n) time on a LinkedList, and O(n) per removal on an ArrayList.
Problem 3
Double everything
Write doubleAll, which replaces every number in the list with twice its value, in place. For [1, 2, 3] the list becomes [2, 4, 6]. Do not build a second list.
Start here
static void doubleAll(List<Integer> numbers) {
throw new UnsupportedOperationException("replace every number with twice its value, in place");
}Paste into jshell to try it
Stuck?
Show a solution
static void doubleAll(List<Integer> numbers) {
ListIterator<Integer> it = numbers.listIterator();
while (it.hasNext()) {
it.set(it.next() * 2); // set replaces the item next() just returned
}
}Paste into jshell to try it
set is allowed because it does not change the list's size, only what is in a slot, and the ListIterator keeps its bookmark in order. Writing numbers.set(i, ...) inside an index loop also works for an ArrayList, but is O(n) per call on a LinkedList, so the iterator is the safe habit. The method takes O(n) time and no extra space.
Iteration in the interview
The questions that come up, and the answers.
Iteration is a favourite because one short bug shows whether you understand how collections work.
- "Why does this loop throw
ConcurrentModificationException?" Because the list was changed outside the iterator while the loop was running. The iterator's remembered version no longer matches the list's. - "How do you remove while iterating?" Use
Iterator.remove(), orremoveIffor a rule. Name both, and say which you would pick. - "What does fail-fast mean?" The iterator checks for changes on each step and throws immediately instead of returning wrong data. Add that it is best-effort: it is a bug alarm, not something to catch and rely on.
- "Is it only a threading problem?" No. The classic case is one thread, one loop, and a
removein the wrong place. The name says "concurrent", but the commonest cause is plain single-threaded code. Lesson 12 comes back to the multi-threaded kind. - "Why does
it.remove()need anext()first?" It removes the itemnext()just returned. With no item returned, there is nothing to remove, so it throwsIllegalStateException. - "Iterator or ListIterator?"
ListIteratorworks on lists only, walks both ways, and cansetandadd. Use a plainIteratorfor everything else. - Say the cost. "A walk is O(n). Removing during the walk is O(1) on a
LinkedListand O(n) per item on anArrayList, soremoveIfis better for many removals."
Updated 2026-10-17
Made by VeraStack Labs