从ArrayList源码探究迭代时修改集合内容会抛出异常的原因
ConcurrentModificationException异常的产生:
在阅读《阿里巴巴 JAVA 开发手册 》一文时,其中有这样一条描述:
7. 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
反例:
List<String> a = new ArrayList<String>();a.add("1");a.add("2");for (String temp:a){if("1".equals(temp)){a.remove(temp);}}说明:这个例子的执行结果会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?
正例:
Iterator<String> it = a.iterator();while (it.hasNext()){String temp = it.next();if("2".equals(temp)){it.remove();}}
对上面描述的foreach循环方法中,将"1"改成"2"
之后,报错如下:
同样的问题:
另外根据集合对象的for增强即为iterator方式循环的简化
的原理,将正例的iterator方式循环中it.remove()
改为a.remove(temp);
也会有同样的报错。
例如:
从源码解释异常的原因
根据异常信息抛出的位置为String temp = it.next();
,可定位异常是由迭代器中的方法抛出的。
查看ArrayList的源码,提取出重点信息如下:
由以上源码可以看出:
- ListArray中有:
属性:int modCount = 0;
(记录对集合的修改操作次数)
内部类:Itr
。(用来返回Iterator对象)
方法:add()
、remove(int index)
、clear()
、trimToSize()
等。(其操作都会修改modCount的值。) - 返回的Iterator对象中有
expectedModCount
属性,初始化时使其等于集合对象中的modCount
。 - Iterator对象中
next()
和remove()
方法在执行时会比较modCount
和expectedModCount
的值,若不相等则抛出ConcurrentModificationException
异常。 - Iterator对象中
remove()
方法在调用集合中的方法remove(int index)
将集合中的对象进行删除以后,会使expectedModCount = modCount;
。
简单的说就是集合对象和迭代对象分别有一个计数器记录修改操作。使用迭代器中remove()
、next()
方法前都会检查两个计数器是否一致,若不一致则抛出异常。迭代器中的计数器只在迭代对象创建和使用迭代器中的remove()
方法后,才会和集合中的计数器同步。若在迭代中调用集合中的方法则会使两个计数器不同步。
fail-fast机制
另外经过查阅以后得知该异常检测机制其实是fail-fast
。
- “快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
对开始时问题的进一步解释
根据前面对源码的分析,得知了 ConcurrentModificationException 异常产生的条件,只要在迭代过程中通过集合本身的方法进行操作就会抛出该异常,然而在最初例子中:
此时虽然是在迭代过程中使用集合本身的方法进行了修改,但是并没有抛出异常。
对上面的例子进行修改,再次测试
使用断点调试一步步执行后,发现问题在于在对倒数第二个对象使用集合内方法进行移除以后,并不会再次进入while循环。此时it.next()并没有执行到,因此不会抛出异常。
再次从上面贴出的源码分析:
在内部类Itr中关于hasNext()方法的定义如下:
- cursor是Itr类中的属性,初始化为0,每当执行next()方法后cursor=cursor+1。
- size则为集合类中的对象的数量。
- 在上面的例子中,移除倒数第二个对象之前,size=3,cursor =2 。
当移除倒数第二个对象后,size=2,cursor =2。 - 执行hasNext()返回false,结束循环。
总结
上面的测试用例中,"1"
、"3"
为条件时抛出异常是因为fail-fast机制
的存在,"2"
为条件时没抛出异常是因为循环已经结束,并没有执行到会检测该异常的it.next()
方法。