示例
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
for (Integer num : list) {
if (2 == num) {
list.remove(num);
}
}
System.out.println(list);
}
先看一段代码。
第909行抛出了异常。
再看看909行,相关方法。
源码
把上述代码编译的class文件取出来。
Java的语法糖在编译期间,把for(;;)
语法编译成了Java中的迭代器iterator()
,这也是抛出异常的主要原因。
ArrayList<Integer> list = new ArrayList();
先new了一个集合对象ArrayList,在ArrayList源码中的第601行,有一个protected修饰的变量modCount
,在集合创建之后,该变量的初始值为0。
protected transient int modCount = 0;
记住这个变量,接着向这个集合中添加了两个元素,执行了add()
方法,看下add()
方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// add()方法中执行了ensureCapacityInternal()方法,这一步的作用是判断add之后容器是否需要扩容。
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 又执行了ensureExplicitCapacity()方法,该方法中,modCount执行了++操作
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在每一次add方法中,modCount
的值+1
在添加了两个元素之后,modCount
的值变成了2
再看main方法的下一行
Iterator var2 = list.iterator();
执行了iterator()
方法
public Iterator<E> iterator() {
return new Itr();
}
在该方法中,new了一个Itr
对象
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
···
}
Itr是ArrayList的内部类,它实现了Java中的迭代器Iterator
类,并且实现了Iterator
类中的hasNext,next,remove基本方法。
在创建Itr的过程中,modCount
变量被赋给了Itr中的expectedModCount
。
此时 expectedModCount = modCount = 2
再看main的下一行
while(var2.hasNext()) {
Integer num = (Integer)var2.next();
if (2 == num) {
list.remove(num);
}
}
编译后的源码中,使用hasNext方法来判断是否已经遍历到数组中的最后一位,而next方法用来移动数组中的指针,帮助我们遍历集合中的元素。
看看next()
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
next()中执行了checkForComodification()
方法。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
该方法比较了一次modCount
和expectedModCount
的值是否相等,若不相等,抛出异常。
遍历第一个元素时,没有问题,第一个元素是1,没有走我们的list.remove()方法。
循环第二遍的时候,2==num,开始执行list.remove(num)方法。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
看看ArrayList中的remove()
方法,remove操作时,modCount
执行了++操作。
此时modCount
由2变成了3。
继续循环,当程序进入
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
方法时,modCount
已经和expectedModCount
不相等了。至此抛出并发修改异常。
这就是Java集合中的快速失败机制。
根本原因就在于,forEach循环的语法会被编译成使用Iterator对象,而Iterator对象每次会调用next()方法,由于遍历过程中一些增删的操作导致ArrayList的modCount的增加,在Iterator的next()方法判断时,expectedModCount并没有被更新,检测不通过。
解决方案
最常用的解决方案,是使用fori或forr的遍历形式,但当正向遍历时,如果执行的是删除操作,则删除之后,ArrayList会调用System.arraycopy(elementData, index+1, elementData, index, numMoved)方法来重新规划集合的大小,很有可能导致数组下标越界异常。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
// 这一步,复制了一个新数组,重新规划了集合的大小。
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
使用反向遍历可以避免这个问题,因为数组从后往前遍历,很难出现数组下标越界。