The idea
A for loop over a collection visits its elements one by one, and it stops when it has visited the last one. That only works if the collection has a fixed end. If the loop body keeps adding elements to the collection it is walking through, the end keeps moving away. A loop that never reaches its end is an infinite loop: the program runs forever and is unusable.
Here is a loop that tries to do exactly that:
val fruits = mutableListOf("banana", "orange")
for (fruit in fruits) {
fruits.add("kivi")
}
Every pass adds a "kivi" to the end of fruits, so there is always one more element to visit.
What Kotlin does about it
On the JVM, the list's iterator remembers how many changes the list had when the loop started. After the first add, the next step of the loop sees that the list was modified behind its back and throws a ConcurrentModificationException. The program stops with an error instead of growing the list forever.
That is helpful, but you shouldn't rely on it. The exception is a safety net, not a rule that lets you modify a list inside its own loop. It isn't thrown every time. If a loop removes elements, the iterator can decide that it has reached the end and stop quietly. After for (x in r) { r.remove(x) } on mutableListOf("a", "b"), the list still holds [b], and nothing was reported. A loop that silently skips elements is a bug that is harder to spot than a crash.
The rule is simple: don't add elements to, or remove them from, the collection a for loop is iterating over.
The exercise
The goal is to add every name from newStudents to the end of students, so that students holds Andy, Mary, Alex, Ana, Alexa and Mike. This is the code you start with:
val students = mutableListOf("Andy", "Mary", "Alex")
val newStudents = mutableListOf("Ana", "Alexa", "Mike")
for (student in newStudents) {
newStudents.add(student)
}
It stops with a ConcurrentModificationException. The mistake is in which list receives the new element:
- Find the list the loop walks over. The header says
for (student in newStudents), sonewStudentsis the list being iterated. - Find the list the body changes. The body calls
newStudents.add(student). It is the same list, so the loop modifies what it is reading. After the first passnewStudentshas four elements,[Ana, Alexa, Mike, Ana], and the iterator throws. - Change the target. The names should end up in
students, so the body must callstudents.add(student). Now the loop reads one list and writes to another, and the list being read never changes.
val students = mutableListOf("Andy", "Mary", "Alex")
val newStudents = mutableListOf("Ana", "Alexa", "Mike")
for (student in newStudents) {
students.add(student)
}
The loop makes three passes, one per name in newStudents, and adds each to the end of students, which then has six elements in the required order.
Kotlin details
mutableListOfis what allowsadd. A list made withlistOfis read-only and has noaddmethod. Both lists here are mutable, so the first version compiles and fails only when it runs.valdoes not make the list constant. It fixes which list the name refers to. You can still add elements to a list stored in aval.- Looping and adding is one way to merge. The call
students.addAll(newStudents)does the same job with no loop at all. - Looping over a copy. If you really must change a list while visiting each element, iterate over a snapshot:
for (x in list.toList()). The copy doesn't change, so the loop ends normally, while changes go to the original.
Common mistakes
- Adding to the list you are looping over. This is the bug in the starting code. Check the name before the dot in
.add(...)against the name in the loop header. - Removing elements inside the loop. It can throw, or it can silently skip elements, depending on the list's size and position.
- Thinking the exception means Kotlin is protecting you from every infinite loop. A
whileloop whose condition never becomes false has no such safety net.
Key takeaways
- Don't change a collection while a
forloop is going through it. - Kotlin on the JVM often reports this with a
ConcurrentModificationException, but not always. - Read from one collection and write to another, or loop over a copy.
- Check which list each line of the body touches.