使用 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,直接跳出循环,没有进入方法检测。