使用 forEach 遍历 ArrayList 时,若直接调用其 remove() 方法,很可能会抛出 ConcurrentModificationException 异常。
场景
1 | ArrayList<String> arrayList = new ArrayList<>(); |
分析
forEach
语法,其底层是调用迭代器来进行遍历的,所以我们来分析 arrayList.remove(s)
和 arrayList.iterator()
这两处关键语句的源码。
首先,我们看 arrayList.iterator()
,方法返回了一个 new Itr()
。
1 | /** |
从 ArrayList
的内部类 Itr
中,可以发现,在 next()
和 remove()
方法里,都存在 checkForComodification()
,该方法便是用来检验,迭代的过程中,实际修改次数与预计修改次数是否相同,若不同,则抛出异常。
1 | final void checkForComodification() { |
我们再看 Itr
类中的 remove()
,其首先调用了 ArrayList.this.remove(lastRet)
,删除了元素,再将 expectedModCount = modCount
进行赋值。所以,调用迭代器的 remove()
方法时,可以通过修改次数方法的检测。
1 | public void remove() { |
我们再看 ArrayList
的 remove(int index)
。当元素被删除时,modCount++
实际修改次数会累加,同时 --size
大小减一。
1 | public E remove(int index) { |
这时,我们就可以理解,由于 forEach
方法里调用的是 arrayList.remove(s)
,modCount
值改变,但是 expectedModCount
没有变化。所以当 forEach
调用迭代器的 next()
时,无法通过检测,抛出异常。
那为什么场景二不会抛出异常呢。这里关键在于,cursor
值的变化。forEach
在循环之前,都会调用迭代器 hasNext()
进行判断。
1 | public boolean hasNext() { |
当调用迭代器的 next()
时,cursor = i + 1
会变到下一个值的下标,lastRet = i
会停留在当前值的下标。
1 | public E next() { |
当调用迭代器的 remove()
方法时,cursor = lastRet
,也就是意味着 cursor
值随着 size
减一。而当调用 ArrayList
的 remove()
时,cursor
并没有变,以至于最后一个值 3
时,cursor == size
,直接跳出循环,没有进入方法检测。