ArrayList之诡异的remove操作

/ Java技术 / 没有评论 / 714浏览

这几天没事的时候把阿里的Java开发手册大概看了一遍,其中有一条如下:

【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。

正例:

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (删除元素条件) {
        iterator.remove();
    }
}

反例:

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

说明:以上代码的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”,会是同样的结果吗?

  这个规则我之前是知道的,但是看到上面这个说明,就有点不太淡定了,难道还会有什么奇葩的事不成。于是把这行代码考到IDEA运行了一下,正例自然是不必说。

  果然remove元素1的时候正常,remove 2java.util.ConcurrentModificationException异常。然后我又添加元素3,这下情况又变了,remove 13异常,remove 2正常。发现一个规律,remove倒数第二元素正常,其他元素就会抛异常。

  因为foreach这种遍历的语法,本身就是Java的语法糖,我们要看真实的代码实现,上述反例经过Java编译器编译后实际上代码如下:

ArrayList list = new ArrayList();
list.add("1");
list.add("2");
Iterator i$ = list.iterator();

while(i$.hasNext()) {
    String item = (String)i$.next();
    if("2".equals(item)) {
        list.remove(item);
    }
}

简单的打了几个断点发现,问题和hasNext()有很大关系。 看一下ArrayList的hasNext()的实现

public boolean hasNext() {
    return cursor != size;
}

看ArrayList的源码,很多方法比如:next()previous()remove()在执行前都会先执行以下方法:

final void checkForComodification() {
    if (expectedModCount != ArrayList.this.modCount)
        throw new ConcurrentModificationException();
}

简单的说checkForComodification()是为了保证访问的一致性。 下面我们通过人肉执行循环来看一下这其中报异常或者不报异常的原因 remove 1的循环如下:

步骤代码表达式值cursorsize备注
1i$.hasNext()true02
2String item = (String)i$.next();-12
3if("1".equals(item))true12
4list.remove(item);-11
5i$.hasNext()false11此时循环结束,走不到下一个next()因此也不会抛出异常

remove 2的循环如下:

步骤代码表达式值cursorsize备注
1i$.hasNext()true02
2String item = (String)i$.next();-12
3if("2".equals(item))false12
4i$.hasNext()true12
5String item = (String)i$.next();-22
6if("2".equals(item))true22
7list.remove(item);-21remove操作完成后modCount != expectedModCount,因为没有执行到next()方法,此时还不会抛出异常
8i$.hasNext()true21
9String item = (String)i$.next();-21此时抛出异常

现在终于明白为什么remove倒数第二个元素不报错的原因了,因为remove倒数第二元素后刚好cursor == size,此时hasNext()返回false。

关于modCountexpectedModCount可参考如下资料: