问题
有使用过集合的都知道,在用 for 遍历集合的时候是不可以对集合进行 remove操作的,因为 remove 操作会改变集合的大小。从而容易造成结果不准确甚至数组下标越界,更严重者还会抛出ConcurrentModificationException。
foreach 遍历等同于 iterator。为了搞清楚异常原因,我们还必须过一遍源码。
源码
public Iterator<E> iterator() {return new Itr();}
原来是直接返回一个 Itr 对象。
从源码可以看出,ArrayList 定义了一个内部类 Itr 实现了 Iterator 接口。在 Itr 内部有三个成员变量。
cursor:代表下一个要访问的元素下标。
lastRet:代表上一个要访问的元素下标。
expectedModCount:代表对 ArrayList 修改次数的期望值,
初始值为 modCount。
下面看看 Itr 的三个主要函数。
hasNext 实现比较简单,如果下一个元素的下标等于集合的大小 ,就证明到最后了。
next 方法也不复杂,但很关键。首先判断 expectedModCount
和 modCount
是否相等。然后对 cursor
进行判断,看是否超过集合大小和数组长度。然后将 cursor
赋值给 lastRet
,并返回下标为 lastRet 的元素。最后将 cursor 自增 1。开始时,cursor = 0,lastRet = -1
;每调用一次 next 方法, cursor 和 lastRet 都会自增 1。
remove
方法首先会判断 lastRet
的值是否小于 0,然后在检查 expectedModCount
和 modCount
是否相等。接下来是关键,直接调用 ArrayList 的 remove
方法删除下标为 lastRet
的元素。然后将 lastRet
赋值给 cursor ,将 lastRet
重新赋值为 -1,并将 modCount
重新赋值给 expectedModCount
。
原因
ArrayList的remove方法删除元素时,会对modCount的值进行增加。modCount是ArrayList的一个字段,它代表ArrayList被结构性修改的次数。所谓结构性修改,是指那些改变ArrayList大小,或者干扰ArrayList迭代过程的修改。
当调用ArrayList的remove方法删除元素时,ArrayList的内部会先将元素删除,然后modCount的值就会加1,代表ArrayList发生了一次结构性修改。这样,当多线程同时修改ArrayList时,通过检查modCount的值,就可以发现其他线程对ArrayList进行的修改,从而避免产生不可预知的错误。
这个机制在ArrayList的迭代器中得到应用。在迭代过程中,迭代器会检查modCount的值是否和预期的一样,如果不一样,就说明在迭代过程中,有其他线程修改了ArrayList,此时迭代器会抛出ConcurrentModificationException异常,告诉用户ArrayList的结构已经被修改。
expectedModCount是ArrayList的内部类Itr(即ArrayList的迭代器)的一个字段,它的值在迭代器创建时被初始化为ArrayList的modCount值。expectedModCount用于在迭代过程中检查ArrayList是否发生了结构性修改。
当调用ArrayList的remove方法删除元素时,会对modCount的值进行增加,但这并不会改变已经创建的迭代器的expectedModCount的值。expectedModCount的值只在迭代器创建时被初始化,之后就不再改变。
然后在迭代过程中,每次调用迭代器的next或remove方法时,都会检查modCount和expectedModCount的值是否相同,如果不同,那就表示在迭代过程中,ArrayList被其他线程进行了结构性修改,此时会抛出ConcurrentModificationException异常。
如果在迭代过程中,通过迭代器的remove方法删除元素,那么在删除元素的同时,会将expectedModCount的值加1,保持和modCount的值同步,这样就不会抛出ConcurrentModificationException异常。
异常的解决
直接调用 iterator.remove() 即可。因为在该方法中增加了 expectedModCount = modCount 操作。但是这个 remove 方法也有弊端。
1、只能进行remove操作,add、clear 等 Itr 中没有。 2、调用 remove 之前必须先调用 next。因为 remove 开始就对 lastRet 做了校验。而 lastRet 初始化时为 -1。
3、next 之后只可以调用一次 remove。因为 remove 会将 lastRet 重新初始化为 -1
remove。因为 remove 会将 lastRet 重新初始化为 -1