现象
List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
list.add("efg");
for (String e:list) {
if ("abc".equals(e)) {
list.remove(e);
}
}
在foreach遍历列表的过程中有删除操作会产生如下报错
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
原因分析
foreach编译后实际是采用迭代器来遍历列表,编译后的class文件内容如下:
List<String> list = new ArrayList();
list.add("abc");
list.add("bcd");
list.add("efg");
Iterator var2 = list.iterator();
while(var2.hasNext()) {
String e = (String)var2.next();
if ("abc".equals(e)) {
list.remove(e);
}
}
从上面的class字节码也可以看出foreach转化成了迭代器来遍历列表,从报错栈可以看到ConcurrentModificationException异常是在ArrayList的迭代器的next函数中出,我们跟进这个函数中看看其中的代码逻辑是怎样的
public E next() {
//1、modCount != expectedModCount时抛异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
int i = cursor;
if (i >= limit)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//2、当前元素索引大于等于数组长度时抛异常
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
该函数中有两处会抛ConcurrentModificationException异常,第2处是多线程的情况下访问ArrayLis会走到,我们主要分析第1处异常是如何抛出的。
第一处异常抛出的条件是modCount != expectedModCount,这两个变量代表什么含义,分别在什么情况下会赋值,搞清楚这两个问题,这个判断条件满足的场景也就清楚了。modCount定义在ArrayList的父类AbstractList中,代表列表被修改的次数,在列表元素数量发生变化的时候该值会发生改变,列表中增加元素或删除元素都会导致该值发生变化,而expectedModCount定义在ArrayList的内部迭代器类中,用于判断遍历的过程中ArrayList是否发生变化,此处的变化是指导致modCount变化的场景。
因此,在用迭代器遍历ArrayList的时候,ArrayList有发生变化的操作,则会抛ConcurrentModificationException异常。此处有一点需要注意,使ArrayList发生变化的操作都是指ArrayList中的方法,如remove、add、clear等方法,而迭代器中有个remove方法,也会导致ArrayList发生变化,但调用该迭代器的remove方法并不会导致抛异常,原因是迭代器中的remove方法在删除元素后会将expectedModCount赋值为modCount,这样就会保证下次再调用到next方法时不会触发抛异常的逻辑。
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
//此处在remove元素后将expectedModCount赋值为modCount
expectedModCount = modCount;
limit--;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
总结
1)在使用foreach方式遍历ArrayList时,实际采用的是迭代器的遍历方式,在遍历的过程中,迭代器的next方法会判断遍历的过程中ArrayList是否发生变化(此处的变化是指ArrayList的元素个数发生变化),如果发生了变化则会抛ConcurrentModificationException,这种方式遍历的过程中如果调用ArrayList的如下方法均会产生此异常:add、addAll、remove、removeAll、clear、retainAll。这里有个前提,调用完这些方法会继续遍历的过程,如果上面的方法
调用后就跳出了遍历过程则不会抛异常,因为没有再走到next方法,抛异常的条件也就不会触发。
2)如果想在遍历ArrayList的过程中改变ArrayList,可使用常规的for循环来遍历(注意正序遍历删除元素会漏删,原因和解决方案可参考此链接),如下:
List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
list.add("efg");
for (int i = 0; i < list.size(); i++) {
String e = list.get(i);
if ("abc".equals(e)) {
list.remove(e);
}
}
如果遍历的过程中仅仅是删除元素,也可以使用迭代器来实现,注意删除的时候使用的是迭代器的删除方法,如过使用ArrayList的remove方法依然会抛异常,如下
List<String> list = new ArrayList<>();
list.add("abc");
list.add("bcd");
list.add("efg");
Iterator it = list.iterator();
while (it.hasNext()) {
String e = (String)it.next();
if ("abc".equals(e)) {
//使用迭代器的删除方法
it.remove();
}
}