Looping over a collection that changes
A for loop walks through a collection one element at a time. That is safe as long as the collection stays the same during the walk. Trouble starts when the loop body changes the very collection being walked, for example by adding elements to it.
In Scala, only a mutable collection can grow, such as scala.collection.mutable.ArrayBuffer. A plain Array has a fixed size and has no += method, and the default List is immutable. Here is a loop that adds to the buffer it is reading:
val fruits = scala.collection.mutable.ArrayBuffer("banana", "orange")
for (fruit <- fruits) {
fruits += "kivi"
}
Each pass adds a new element to the end. If the loop kept going until it reached the end, it would never get there: every pass moves the end one step further away. That is an infinite loop, a program that never finishes.
What Scala does about it
An ArrayBuffer keeps track of how many times it has been modified. Its iterator checks that count on each step, and when the buffer has changed since the loop began, it throws an exception instead of continuing. The loop above stops after the first pass with:
java.util.ConcurrentModificationException: mutation occurred during iteration
This check is a safety net, not a feature to rely on. Other collections may react differently, and the lesson is that changing a collection while looping over it is a bug whichever way it fails.
The exercise
The editor holds two buffers: students with "Andy", "Mary" and "Alex", and newStudents with "Ana", "Alexa" and "Mike". The goal is to add every name from newStudents to the end of students. The starter loop reads newStudents and, in its body, appends to newStudents too:
val students = scala.collection.mutable.ArrayBuffer("Andy", "Mary", "Alex")
val newStudents = scala.collection.mutable.ArrayBuffer("Ana", "Alexa", "Mike")
for (student <- newStudents) {
newStudents += student
}
It reads from a buffer and writes to the same one, so it throws after the first pass. On that pass newStudents has become ArrayBuffer(Ana, Alexa, Mike, Ana), and students still has only its three original names.
- Decide which buffer is read and which is written. The loop should read
newStudentsand write tostudents. - Keep the header.
for (student <- newStudents)is already right. - Change the target of
+=fromnewStudentstostudents. Now the buffer being walked is never touched.
val students = scala.collection.mutable.ArrayBuffer("Andy", "Mary", "Alex")
val newStudents = scala.collection.mutable.ArrayBuffer("Ana", "Alexa", "Mike")
for (student <- newStudents) {
students += student
}
After the loop, students holds Andy, Mary, Alex, Ana, Alexa, Mike, in that order. The solution only builds the buffer. To see the six names, loop over students afterwards and print each one.
Scala details
+=on a buffer appends. On anArrayBuffer,buffer += xaddsxat the end and changes the buffer in place. Even thoughstudentsis aval, that is allowed:valstops you from pointingstudentsat a different object, not from changing the object's contents.- An
Arraycan't grow.arr += 3on anArray[Int]doesn't compile:value += is not a member of Array[Int]. - Looping over a copy is safe. If you really want to add to a buffer for every element it already has, loop over a snapshot such as
buffer.toList. Starting fromArrayBuffer(1, 2, 3),for (x <- buffer.toList) buffer += xends withArrayBuffer(1, 2, 3, 1, 2, 3), because the loop walks the copy.
Key takeaways
- Don't add to or remove from a collection while a
forloop is walking it. - An
ArrayBufferdetects this and throwsConcurrentModificationException. - Read from one collection and write to another, or loop over a copy.
