ArrayList iterator源码分析,为什么迭代器删除元素不会报错

写这篇文章的原因要从前不久的一件事说起。

有一天,朋友问我,“ArrayList遍历中删除元素会怎么样”?

我当时脑子里第一想到的就是forEach这种循环方式,没多想就回答他:“当然会报错了。”

朋友再问:“如果使用iterator迭代器遍历删除呢?”

我:“那不会报错,可以正常删除。”

“为什么?”

为什么?我思索半刻,脑子里没有答案。

心想这种问题实在不该答不出来,只是自己在学习过程中确实没有关注过这些细节,只知道“会报错”或“不会报错”而已。

朋友见我不答,便又带几分引导的问:“forEach和iterator使用的是同一个remove方法吗?”

我又沉默,不久讪笑着说道:“不清楚。”

事后,我反思了自己的问题:在学习上不够仔细,只知其然而不知其所以然,虽也足以应对工作,但若是想在开发这条路上继续走下去,则是万万要改正的。

基于此教训,我也是开始关注这些“细枝末节”,并成文做记录,此就是其中一篇。

废话讲完,先看看下面的例子。

// forEach方式
ArrayList<String> l = new ArrayList<>();
l.add("1");
for (String s : l) {    // ----- 结果:java.util.ConcurrentModificationException
    l.remove(s);
}

// 迭代器方式
ArrayList<String> l = new ArrayList<>();
l.add("1");
l.add("2");
Iterator<String> iterator = l.iterator();
while (iterator.hasNext()){
    String next = iterator.next();
    iterator.remove();   // ----- 结果:正常删除
}

正如我对朋友所说,forEach会报错,Iterator能够如我们期望的删除掉元素。那它们两者有什么区别呢?朋友问我它们用的是不是同一个remove方法又是何意?

forEach也不总是报错,会有巧合不报错的情况。

forEach语法

我们先来说一下forEach这个语法,java文档中叫它“The enhanced for statement”,翻译过来就是“增强for循环”。它是Java提供的一个语法糖,经过编译器的翻译后(javac命令),它会被翻译成普通的for循环。

ArrayList<String> l = new ArrayList<>();
l.add("1");

for (String s : l) {
    l.remove(s);
}

以上代码在经过javac编译后的结果如下:

ArrayList<String> l = new ArrayList();
l.add("1");
Iterator var2 = l.iterator();

while(var2.hasNext()) {
    String s = (String)var2.next();
    l.remove(s); // ----- 用的是ArrayList的remove方法
}

可以看到,原本forEach的部分被自动替换成了Iterator,方法里通过Iterator#hasNext来控制循环次数、通过Iterator#next方法来获取下一个元素。乍一看,好像和文章开头我们写的Iterator示例代码一样,但仔细看就会发现它的删除操作是使用了ArrayList本身的remove方法。

正是这个删除操作的不同,会导致我们在使用forEach删除元素时抛出ConcurrentModificationException异常,要搞清楚抛出异常的原因,我们需要先来看一下ArrayListremove方法。

使用增强for循环的表达式必须是Iterable接口的子类或是一个数组类型,否则会发生编译时错误。

至于数组类型的forEach会被翻译成什么样子,读者不妨自己亲自编译看下。

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;
}

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;
}

代码很简单,就是遍历找到第一个相等的元素,然后把它从当前数组里剔除掉。元素的删除是由System.arraycopy方式完成的,删除之后维护一下size字段。

需要特别注意的是modCount++这个操作,该字段记录list在结构上被修改的次数。 结构修改是指那些改变list大小的修改,此字段由类IteratorListIterator使用,如果此字段的值意外更改,Iterator(或ListIterator)将抛出 ConcurrentModificationException 以响应 next、remove、previous、set add 操作。

如下贴出Iteratorremove方法,报错的原因就一目了然:

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() {}

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

    @SuppressWarnings("unchecked")
    public E next() {
        // ...
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();           // ----- 看这里
        /*
         * final void checkForComodification() {
         *     if (modCount != expectedModCount)
         *         throw new ConcurrentModificationException();
         * }
         */

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;     // ----- 看这里
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        // ...
    }

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

至此,文章结束,我们再回答文章一开始的问题,“forEach和iterator使用的是同一个remove方法吗”?答:是,只不过iterator里面包装了一些处理modCount字段的逻辑。

参考目录

1 :

Java Language Specification

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值