熟悉Java的朋友都知道,使用list中的remove方法进行遍历删除时,会有不少的坑,今天就从源码的角度分析记录一下。
案例

如上图所示,正常逻辑我们会选择如上四种方式进行删除集合中的全部元素或者某个元素。
方式一
当我们使用第一种foreach方式进行删除时,代码执行到第二个元素就直接抛出异常,异常如下:

程序抛出了java.util.ConcurrentModificationException异常, 我们知道foreach 遍历等同于 iterator,接下来看源码:
代码块一
public Iterator<E> iterator() {
return 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() {}
//foreach第一步
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 {
//此处remove还是调用的ArrayList中的remove方法
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();
}
}
这俩段代码块都是从ArrayList源码类中复制的,从源码可以看出,ArrayList 定义了一个内部类 Itr 实现了 Iterator 接口。在 Itr 内部有三个成员变量:
cursor:代表下一个要访问的元素下标。
lastRet:代表上一个要访问的元素下标。
modCount: 成员变量,记录着集合的修改次数,也就每次add或者remove它的值都会加1。
expectedModCount:代表对 ArrayList 修改次数的期望值,初始值为 modCount。
下面看看 Itr 的三个主要函数。
hasNext 实现比较简单,如果下一个元素的下标等于集合的大小 ,就证明到最后了。
next 方法也比较简单,但是比较关键,首先判断 expectedModCount 和 modCount 是否相等。然后对 cursor ,lastRet 进行判断赋值,最后我们可以得知,每调用一次 next 方法, cursor 和 lastRet 都会自增 1。
next方法走完后我们再回到伪代码中:
代码块三
for (String s : strings) {
strings.remove(s);
}
此刻开始调用remove方法,注意是 ArrayList 的 remove,而不是 Itr 的 remove,来看下ArrayList 的 remove源码:
代码块四
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;
}
//判断当前索引是否大于等于list长度,如果满足则抛出数组越界异常
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
如上代码块中,判断完当前索引是否大于等于list长度后,对modCount加1,所以第二次循环来到 next 方法,因为上一步的 remove 方法对 modCount 做了修改但是并没有设置expectedModCount = modCount,致使 expectedModCount 与 modCount 不相等,这就是ConcurrentModificationException 异常的原因所在,所以不符合。
当cursor == size,hasNext() 判断没有元素了,不再调用 next() 方法。
方式二
使用原始for循环进行,此时调用的就是ArrayList的remove方法,然后查阅上面代码块便可得知,在刚开始就会去执行rangeCheck方法去做校验,当索引大于等于list长度时就不会再执行了,所以也不符合。
方式三
看了源码后那就会知道,这种方式是完全满足的。
方式四
回到代码块二便可知,直接调用内部类Itr中的remove方法就可解决expectedModCount != modCount 问题 。因为在该方法中增加了 expectedModCount = modCount 操作。
iterator.remove()弊端
1.remove()将会删除上次调用next()时返回的元素,也就是说先调用next()方法,再调用remove方法才会删除元素。next()和romove方法具有依赖性,必须先用next,再使用romove。如果先用remove方法会出现IllegalStateException异常。
2.使用remove()方法必须紧跟在next()之后执行,如果在remove和next中间,集合出现了结构性变化(删除或者是增加)则会出现异常IllegalStateException。
3.next 之后只可以调用一次 remove。因为 remove 会将 lastRet 重新初始化为 -1。
疑问
JDK为什么会设计expectedModCount与modCount这俩个成员变量?
在ArrayList,LinkedList,HashMap等等的内部实现增,删,改中我们总能看到modCount的身影,而且这几个集合的公共特点就是所有使用modCount属性的全是线程不安全的,所以如果在使用迭代器的过程中有其他线程修改了此集合中的内容,对集合中内容的修改都将增加这个值,然后在迭代器初始化过程中会将这个值赋给expectedModCount,然后进行对expectedModCount与modCount的比较,如果不相等就表示已经有其他线程修改了 List,那么将抛出ConcurrentModificationException异常,这其中也涉及了fail-fast(快速失败)策略,后面再分析。