forEach 的 ConcurrentModificationException 异常

使用 forEach 遍历 ArrayList 时,若直接调用其 remove() 方法,很可能会抛出 ConcurrentModificationException 异常。

场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("1");
arrayList.add("2");
arrayList.add("3");

// 场景一,抛出异常
// java.util.ConcurrentModificationException

for (String s : arrayList) {
if ("1".equals(s)) {
arrayList.remove(s);
}
}

// 场景二,正常
// 输出 [1, 3]

for (String s : arrayList) {
if ("2".equals(s)) {
arrayList.remove(s);
}
}

// 场景三,抛出异常
// java.util.ConcurrentModificationException

for (String s : arrayList) {
if ("3".equals(s)) {
arrayList.remove(s);
}
}

分析

forEach 语法,其底层是调用迭代器来进行遍历的,所以我们来分析 arrayList.remove(s)arrayList.iterator() 这两处关键语句的源码。

首先,我们看 arrayList.iterator(),方法返回了一个 new Itr()

1
2
3
4
5
6
7
8
9
10
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/

public Iterator<E> iterator() {
return new Itr();
}

ArrayList 的内部类 Itr 中,可以发现,在 next()remove() 方法里,都存在 checkForComodification(),该方法便是用来检验,迭代的过程中,实际修改次数与预计修改次数是否相同,若不同,则抛出异常。

1
2
3
4
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

我们再看 Itr 类中的 remove(),其首先调用了 ArrayList.this.remove(lastRet),删除了元素,再将 expectedModCount = modCount 进行赋值。所以,调用迭代器的 remove() 方法时,可以通过修改次数方法的检测。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

我们再看 ArrayListremove(int index)。当元素被删除时,modCount++ 实际修改次数会累加,同时 --size 大小减一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public E remove(int index) {
rangeCheck(index);

modCount++;
E oldValue = elementData(index);

int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

这时,我们就可以理解,由于 forEach 方法里调用的是 arrayList.remove(s)modCount 值改变,但是 expectedModCount 没有变化。所以当 forEach 调用迭代器的 next() 时,无法通过检测,抛出异常。

那为什么场景二不会抛出异常呢。这里关键在于,cursor 值的变化。forEach 在循环之前,都会调用迭代器 hasNext() 进行判断。

1
2
3
public boolean hasNext() {
return cursor != size;
}

当调用迭代器的 next() 时,cursor = i + 1 会变到下一个值的下标,lastRet = i 会停留在当前值的下标。

1
2
3
4
5
6
7
8
9
10
11
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

当调用迭代器的 remove() 方法时,cursor = lastRet,也就是意味着 cursor 值随着 size 减一。而当调用 ArrayListremove() 时,cursor 并没有变,以至于最后一个值 3 时,cursor == size,直接跳出循环,没有进入方法检测。