阿里Java开发手册中有这么一条对foreach中进行remove/add操作的约束
在反例中,如果判断条件为"1"不会报错,但是如果判断条件变成"2"则会抛出java.util.ConcurrentModificationException异常
下面我们先来分析下为什么"2"会报错
通过报错的堆栈信息可以看出是从at java.util.ArrayList$Itr.next(ArrayList.java:859)
方法抛出的异常,但是我们并没有调用next方法啊?
通过查看字节码可以看到foreach其实就是通过while+iterator来实现的
List<String> list = new ArrayList();
list.add("1");
list.add("2");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String s = (String)var2.next();
if (s.equals("2")) {
list.remove(s);
}
}
先看一下iterator是如何判断还有下一个元素的
public boolean hasNext() {
return cursor != size;
}
// cursor 游标,初始值为0,每一次调用next()自增1
// size 集合大小
点到源码中看一下,next函数的第一行就是一个校验
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// modCount是记录集合改变的次数,调用add、remove方法都会使其递增
// 而expectedModCount是用于记录modCount的初始值
hasNext | modCount | expectedModCount | |
---|---|---|---|
初始值 | 2 | 2 | |
第一次while,不满足equals | 0 != 2 true | 2 | 2 |
第二次循环,满足equals | 1 != 2 true | 3 | 2 |
第三次循环 | 2 != 1 true | - | - |
问题就处在第二次循环删除了一个元素后,modCount已经不等于expectedModCount了,但是第三次循环因为元素被删除导致hasNext为true,继续执行next走进checkForComodification校验抛出的异常
所以在迭代器中调用list自身的remove或者add方法都会抛异常,建议使用迭代器的remove方法
至于案例一不报错,就是属于一种巧合的情况了
hasNext | modCount | expectedModCount | |
---|---|---|---|
初始值 | 2 | 2 | |
第一次while,满足equals | 0 != 2 true | 3 | 2 |
第二次循环,不满足equals | 1 != 1 false | - | - |
由于第一次循环就删除了一个元素,在下次循环hasNext的判断时 cursor 刚好等于 size就返回false直接结束循环了,从而没机会调用next抛异常,同理如果刚好删除的是集合倒二位的元素都不会报错
至于hasNext为什么不直接使用cursor < size
来判断,我认为可能是ConcurrentModificationException本身设计就是为了使某些集合在进行迭代时检测到修改而快速失败的,该设计本身就不允许或不建议开发者进行这种操作。