ArrayList在foreach正常迭代删除不报错的原因

时间:2021-06-12 19:05:20

一、背景

在以前的随笔中说道过ArrayList的foreach迭代删除的问题:ArrayList迭代过程删除问题

按照以前的说法,在ArrayList中通过foreach迭代删除会抛异常:java.util.ConcurrentModificationException

但是下面这段代码实际情况却没报异常,是什么情况?

     List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String item : list) {
System.out.println("item:" + item);
if ("1".equals(item)) {
list.remove(item);
}
}

二、分析

我们知道ArrayList的foreach迭代调用的是ArrayList内部类Itr,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; public boolean hasNext() {
return cursor != size;
} @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];
} public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
} final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

按照调用顺序查看源码

1.hasNext

ArrayList的foreach迭代首先调用的是ArrayList内部类ItrhasNext(),该方法会对当前循环指针和长度做判断。只有当hasNext()返回true才会执行foreach里面的代码,cursor = size时候就退出循环(因为这两者相等意味着都遍历完了,假如ArrayList的size=2,那么hasNext会被调用3次,但是第3次调用不会执行foreach里面代码)。

2.如果上一步返回true的话会执行Itr的next(),如果数据无异常的话 cursor = i + 1;所以没执行一次next()时cursor都会+1

3.接着会执行到 list.remove(item),此处调用的是ArrayList的remove(Object o)而不是Itr的,看下ArrayList的remove()的源码:

 public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

o != null,会进入else的fastRemove(index);可以看到ArrayList根据传入的值删除会进行遍历equals判断,找到索引再通过fastRemove(index)删除,因此List频繁做删除修改效率比较低。

4.再看下fastRemove()源码:

    */
private void fastRemove(int index) {
modCount++;
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
}

第8行会把该索引对应的数组的值置为null,并且size-1。但是却没有进行 cursor - 1操作

至此明白了。此处的ArrayList通过foreach迭代删除为什么不会报错:

刚开始ArrayList的size=2时,cursor =0

①第一次hasNext()进来,cursor != size进入next()后cursor=1,接着因为满足条件删除的时候size-1=1;

②第二次hasNext()进来,cursor = size = 1,所以不会执行foreach的代码,也不会出现后面检测modCount值抛ConcurrentModificationException

上述未抛异常的情况主要是hasNext()中判断遍历完成的条件与ArrayList删除后的数据刚好吻合而已。

所以只要满足条件:删除的元素在循环时的指针cursor+1=size就会出现这种情况!删除ArrayList倒数第二个(即第 size - 1个元素)就会出现不抛异常的假象。

(例如size=3,删除第2个元素;size=4,删除第3个元素)

因为删除后size-1=cursor

public boolean hasNext() {

return cursor != size;

}

hasNext()会返回false,不会进入ArrayList的迭代器,就不会进入 next() 执行checkForComodification()

这是一种条件判断下的特殊情况,其他情况都会抛出异常,所以不要在foreach进行删除数据。请在迭代器中进行删除。