深刻理解Java集合中的Iterator

👉本文章全部文字纯原创,若是须要转载,请注明转载出处,谢谢!😘java

问题由来

之因此今天想写这篇文章彻底是一个偶然的机会。昨晚,微信技术群里的一位猿友@我,问了我一个问题,代码以下。他问我,这样写有没有问题,会不会报错?而后他说这时他今天去面试的面试官出的题目,他回答不出来。😅git

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("3");
        list.add("5");

        for (Object o : list) {
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}
复制代码

我当时没仔细想,感受挺简单的问题,😏但定睛一看,这个是在一个加强for循环中执行了一个list的remove方法。有点Java基础的基友们确定都知道,用迭代器方式遍历集合元素时,若是须要删除或者修改集合中元素,必需要使用迭代器的方法,绝对不能使用集合自身的方法。我也一直把这句话视为铁律。因而我判定,这个代码是有问题的,确定会报错的。而后我噼里啪啦一顿操做猛如虎,把这段代码敲了一遍,一顿运行......输出结果以下:github

sca-1

结果傻眼了,竟然正常输出没有报错,并且结果仍是正确的!因而我又改动了一下代码,以下:面试

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
      	//其他代码都没有修改,就在list.add("3");以前添加这一行
        list.add("2");
        list.add("3");
        list.add("5");

        for (Object o : list) {
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}
复制代码

输出结果以下:后端

sca-2

发现结果仍是正确的。微信

因而我又改动了一下代码,以下:多线程

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
      	//其他代码都没有修改,就在list.add("3")以后添加这一行
        list.add("4");
        list.add("5");
        for (Object o : list) {
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}
复制代码

输出结果以下:并发

sca-3

这一次终于出现了期待已久的报错。ide

真的是奇哉怪也,竟然会有如此不一样的运行结果?! 这下让我意识到了问题的严重性,这个问题并无之前理解的那么简单。再加上本身打破砂锅问到底的性格,因而决定好好来研究一番,顺便写点东西,一方面本身之后能够回顾,也能够和各大佬交流技术,不亦乐乎?😎源码分析

源码分析

ConcurrentModificationException

追根溯源,既然程序抛出该异常,那么固然先要把这个异常搞清楚。秉承学技术一看官方文档二看源码的习惯,我就看了一下ConcurrentModificationException的javadoc,原文很是长,这边贴一部分关键的,有兴趣的能够本身去翻阅JDK源码。

/** * This exception may be thrown by methods that have detected concurrent * modification of an object when such modification is not permissible. * For example, it is not generally permissible for one thread to modify a Collection * while another thread is iterating over it.Some Iterator * implementations (including those of all the general purpose collection implementations * provided by the JRE) may choose to throw this exception if this behavior is * detected. Iterators that do this are known as <i>fail-fast</i> iterators, * as they fail quickly and cleanly, rather that risking arbitrary, * non-deterministic behavior at an undetermined time in the future. * Note that this exception does not always indicate that an object has * been concurrently modified by a <i>different</i> thread. If a single * thread issues a sequence of method invocations that violates the * contract of an object, the object may throw this exception. For * example, if a thread modifies a collection directly while it is * iterating over the collection with a fail-fast iterator, the iterator * will throw this exception. */
复制代码

​ 这一大段话大概意思是说,这个异常可能会在检测到一个对象被作了不合法的并发修改,好比jdk自带的集合一般会内置一个fail-fast类型的迭代器,当集合检测到这类不合法的并发修改,就会抛出该异常。所谓的fail-fast,顾名思义,就是当检测到有异常时,越快抛出异常结束越好,以避免未来带来未知的隐患。另外这段话还说了,这个异常并非像名字那样只会出如今多线程并发修改的状况下,在单线程下也会出现。

​ 然并卵,看了半天文档仍是一脸懵逼。这到底说的是什么鬼?

​ 不要紧,控制台除了抛出这个异常,还提示了具体的异常抛出的位置,在java.util.ArrayList$Itr.next()内部的checkForComodification()方法。定位到ArrayList源码指定位置,以下图标识红框位置:

sca-4

这个方法的逻辑很是简单。

sca-5

那modCount和expectedModCount又是何方神圣?跟着来到定义他们的地方。

modCount

modCount是定义在AbstractList(ArrayList的父类)里面的一个属性。这个属性的javadoc也是至关长,我挑选一部分给你们看一下。

/** * The number of times this list has been <i>structurally modified</i>. * Structural modifications are those that change the size of the * list, or otherwise perturb it in such a fashion that iterations in * progress may yield incorrect results. * <p><b>Use of this field by subclasses is optional.</b> If a subclass * wishes to provide fail-fast iterators (and list iterators), then it * merely has to increment this field in its {@code add(int, E)} and * {@code remove(int)} methods (and any other methods that it overrides * that result in structural modifications to the list). A single call to * {@code add(int, E)} or {@code remove(int)} must add no more than * one to this field, or the iterators (and list iterators) will throw * bogus {@code ConcurrentModificationExceptions}. If an implementation * does not wish to provide fail-fast iterators, this field may be * ignored. */
protected transient int modCount = 0;
复制代码

大概意思是,这个字段的值是用来记录list被结构性操做的次数。何为结构性操做?就是对List的容量有影响的或者迭代过程当中会致使错误结果的操做。而子类可使用这个字段的值来实现fail-fast。若是要实现fail-fast,须要在全部结构性操做的方法内部作modCount++操做,而且每一个方法内部只能增长一次。若是不想实现fail-fast就不须要这个值的。好比ArrayList的add方法里面就有modCount++操做,以下图所示:

sca-9

expectedModCount

再来看看expectedModCount。expectedModCount是定义在java.util.ArrayList$Itr里面的属性,而且会将ArrayList的modCount的值做为其初始化值。

sca-6

看到这里是否是有点感受了?也就是正常状况下,ArrayList初始化后,内置的Itr也跟着初始化,而且expectedModCount和modCount是保持一致的。若是没有进行迭代操做,天然是不会出现不一致的问题,也就不会抛出ConcurrentModificationException。那咱们的程序到底为何会致使这两个值不一致呢?此时,不使用大招——debug我反正是机关用尽了。由于咱们的程序中使用了一个加强forEach循环,其实forEach能够看作是jdk一个语法糖,底层就是使用迭代器实现的。因此为了看清楚,咱们在java.util.ArrayList$Itr的方法上都加上断点。以下图:

sca-7

咱们就以开头的那三个例子最后一个报错的为例,开始debug。

刚开始list添加了5个元素,size等于5。由前面得知,add操做属于结构性操做,会致使modCount++

sca-8

Itr迭代器的游标cursor值会从0开始随着元素的遍历移动。hasNext()经过判断cursor != size来肯定list是否还有下一个元素取出。若是返回true,则会进入next()用来返回下一个元素。

sca-10

显然咱们有5个元素,能够进入next()。而在next方法中,第一行代码就是checkForComodification()用来校验expectedModCount和modCount的一致性。显然从List添加完元素到如今为止,咱们没有再对list有过额外的结构性操做,天然前面3次迭代都不会抛出异常,正常返回元素。都如图所示。

sca-11

而且每次执行完next()后,cursor会日后移动一位,为迭代下一个元素作准备。

sca-12

这个时候轮到迭代第三个元素"3"了。天然if条件判断成立,会进入删除操做。

sca-13

跟进remove()方法源码中,确实发现了modCount++。也就是说,这个时候modCount值已经变成6了。而expectedModCount依然仍是保存着初始值5。此时二者不一致了。

sca-14

sca-15

由于list在“3”以后还有“4”,“5”两个元素,所以当删除“3”元素以后,迭代器还会继续迭代,重复以前的流程,会先进入hasNext(),此时cursor等于3,size等于4,天然仍是知足的,因此仍是会继续进入next()取出下一个元素。

sca-16

能够预料此时checkForComodification()校验expectedModCount和modCount已经不一致了,因此抛出了ConcurrentModificationException。

sca-17

初步总结

也就是说,在forEach或者迭代器中调用对集合的结构性操做会致使modCount值发生修改,而expectedModCount的值仍然是初始化值,因此在next()中校验不经过抛出异常。这也是为何之前刚学习迭代器的时候,各大佬叫我不要在迭代器迭代过程当中使用集合自带的remove等操做,而要使用迭代器自带的remove方法,缘由就在于此。那为何使用迭代器自带的remove方法就不会报错呢?以下代码:

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        list.add("5");
        for (Iterator it = list.iterator(); it.hasNext(); ) {
            if ("3".equals(it.next()))
                it.remove();
        }
        System.out.println(list);
    }
}
复制代码

这是老师教的正确姿式。结果固然是正确的。

sca-18

再探虎穴

要搞清楚这中间的区别,固然仍是须要深刻虎穴,再去看看List迭代器remove方法的源码了。下面代码中主要关注红框的2行,第一行做用是删除被迭代的元素,ArrayList.this.remove这个是调用外部类ArrayList的remove方法,上面已经说过了,集合的remove方法是结构性操做,会致使modCount++的,这样等迭代下一个元素时,调用next()时校验expectedModCount和modCount一致性必然会报错,为了防止这个问题,因此下一行expectedModCount = modCount将expectedModCount更新至modCount最新值,使得一致性不被破坏。这也是为何使用迭代器自带的remove方法并不会抛出异常的缘由。

sca-19

怎么样?是否是感受大功告成了,感受本身要飘了......

一气呵成

然而,这只是解释了文章开头3个例子的最后一个,那为何前两个能够正常删除没有报错?说实话,我当时遇到这问题的心里是崩溃到怀疑人生的。

仍是没有好的办法,继续来debug一下前面的例子,看看会有什么不一样的事情发生吧。

List中前面的元素的遍历过程和上面是同样的,再也不赘述。直接看关键处,以下图,这个时候已经遍历到“3”这个元素了,即将开始remove操做,remove操做也和上面同样,会调用fastRemove()删除元素,fastRemove()也确实会执行modCount++,确实致使了expectedModCount != modCount。可是......

sca-20

当将要迭代下一个元素的时候,仍是会进入hashNext()作判断,很遗憾,这个时候cursor和size都是2,也就是hashNext()条件不成立返回false,也就不会再进入next()方法,天然也就不会再去调用checkForComodification()作校验,也就不会再有机会抛异常了。其实这个时候,list中最后一个元素"5"根本也就没遍历到。为了验证这一点,能够在for循环中添加输出代码:

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("3");
        list.add("5");

        for (Object o : list) {
            System.out.println(o);//输出正在迭代的元素
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}
复制代码

会发现只会输出1和3。

sca-22

事情还没完,最后再来一种状况,代码以下:

public class CollectionDemo {
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add("1");
        list.add("2");
        list.add("3");
        for (Object o : list) {
            if ("3".equals(o))
                list.remove(o);
        }
        System.out.println(list);
    }
}
复制代码

猜猜结果是啥?有人会认为,不是和文章第一个例子如出一辙的吗?那就是成功删除了啊,输出1和2啊。呵呵🙄,让您失望了。

sca-23

是否是又怀疑人生了?其实有了前面这么多的铺垫,这个错误缘由已经不难推断发现了。

缘由仍是在这里。前面“1”,“2”两个元素遍历完毕确定是没问题的,当开始遍历“3”时候,经过next()返回元素“3”,cursor此时会增长到3,而size因为后面会调用remove减为2了,这个时候hasNext()里的条件返回true又成立啦!个人乖乖......因此Itr迭代器又会傻傻的去调用next(),后面的事情就都知道了,checkForComodification()又被调用了,抛出ConcurrentModificationException异常。

sca-24

其实经过上述的整个分析过程,能够总结出一点结论:其实整个过程的问题关键所在就是java.util.ArrayList$Itr的hasNext()方法的逻辑。不难看出,每当迭代器返回一个元素时,元素在列表中的索引等于Itr的cursor值,而每次删除一个元素会致使size--,不难推断出,若是你要删除的元素刚好位于List倒数第二个位置,则并不会抛出异常,而且会显示正确的删除操做,就像文章开头第一个例子,其他状况都会抛出异常。可是就算是不抛异常的状况,其实此时List迭代器内部的expectedModCount 和modCount一致性已经遭到了破坏,只是被掩盖了,因此这样的操做后续可能会有很是大的隐患,我的不建议这样使用,须要在迭代过程操做集合的仍是应该用迭代器的方法。

另外,其实除了ArrayList之外,会发现HashMap中也会有modCount属性,而在其相应的结构性操做方法内部,如put()、clear()等都会有对modCount++操做,而在HashMap内部也有一个内部迭代器HashIterator,内部会维护一个expectedModCount属性,其他的套路就都和ArrayList相似了。


  • 今天的技术分享就分享到这里,感谢您百忙抽出这么长时间阅读个人文章😊。
  • 另外,个人笔记还有文章也会在个人github上更新。
相关文章
相关标签/搜索