ArrayList中Iterator的研究学习

最近去某公司面试,被问到了一个简单的问题,ArrayList中要删除一些元素应该怎么操作?答曰:"使用Iterator迭代器遍历,判断之后再用迭代器提供的remove()方法将判断为true的元素删掉",问:“为什么要选择这个方法?”答曰:“迭代器采用的是fail-fast机制,foreach循环内部调用了迭代器,删除元素破坏了集合的结构,所以会报错”,追问:“为什么不能用for循环呢?没使用迭代器也应该可行啊?为什么迭代器又是可以的呢?” 答曰:“因为for循环删掉元素后会错位,至于迭代器是怎么解决这个问题的, 还真不知道...”

作为一名菜鸟,招架不了这么连珠炮般的提问,于是回去读了一遍关于Iterator的源码,简要的解析一番。本次学习过程大概是围绕这几个问题展开的:

 

       1、Iterator和Iterable的关系

Iterator是java中的一个接口,定义了一系列方法。诸如hasNext()判定、next()取下一元素的值和remove()删除当前元素值。

Iterable是java中的一个接口,定义了一个Iterator的实现类作为成员变量,也提供了for-each这个语法糖。

正如它们的英文意思,Iterator规定了作为一个迭代器应该具有的方法,而Iterable则是规定了如果一个类可以被迭代,不仅要自己实现属于自己的迭代器,而且还附送一个for-each语法糖给程序员作为快捷手段。

   2、fail-fast机制  

  • 什么是fail-fast?
      The iterators returned by this class‘s {@link #iterator() iterator} and
 * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a>
 * if the list is structurally modified at any time after the iterator is
 * created, in any way except through the iterator‘s own
 * {@link ListIterator#remove() remove} or
 * {@link ListIterator#add(Object) add} methods, the iterator will throw a
 * {@link ConcurrentModificationException}.  Thus, in the face of
 * concurrent modification, the iterator fails quickly and cleanly, rather
 * than risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.

这个官方解释已经非常明白了:为防止并发(可以理解为一切Iterator体系之外的方法)修改,迭代器一旦被调出来之后,任何除了Iterator类中定义的方法以外的方法(包括集合的clear、remove和add方法),对集合元素进行修改,都属于结构化的改变(无论是别的线程还是自己的线程),迭代器就会抛出ConcurrentModificationException的错误。这里的结构改变,确切的说指的是集合的size发生变化。(注意:并非是elementData这个数组的length发生改变,因为数组中的元素完全可以为null,length不变,而集合中这个元素删掉了,size就得减1)。

  • 为什么会报错?

在这里就不贴代码了,直接上结果:

    • ArrayList提供了一个成员变量modCount,这个变量表示整个list被结构化修改的次数,初始值为0。
    • ArrayList的remove、add、clear方法执行的时候,内部会执行modCount++
    • ArrayList中的迭代器中提供一个成员变量expectedModCount,初始值为modCount,记录在迭代器被创建时的集合结构改变的次数。
    • 迭代器的next、remove方法在执行之前都会调用checkForModification方法判定expectedModCount与modCount之间是否相等,如果不相等,则抛出concurrentModificationException

这就可以解释为什么for-each不能在内部使用集合的删除方法了——因为它底层调用了Iterator的next方法进行遍历。

        3、为什么Iterator.remove()不会出现错位?

这也是Iterator设计的精髓所在。

        private class Itr implements Iterator<E> {
        int cursor;       // 即将取出的元素的角标
        int lastRet = -1; // 上一次取出的元素的角标

        int expectedModCount = modCount;       // Iterator创建时的改动初始值
        public boolean hasNext() {
            return cursor != size;      //判断是否还有下一位元素存在,该方法要实现持续遍历判断,必须要配合next()方法
        }

        @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;                       // 更新cursor的值为下一轮判定做准备
            return (E) elementData[lastRet = i];  // 给lastRet赋值,否则remove()方法报错
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;    //对cursor进行修正
                lastRet = -1;       //remove()方法不能连续执行,因为lastRet不合法,只能先执行next()
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
  • 将迭代器设计成内部类

因为迭代器需要实现接口中操作对象的方法,而不同的对象其底层数据结构不同,因此迭代器的实现类需要根据对象的特点进行设计,也就是说迭代器只有在相应的对象中才有用,不如就在其内部定义得了。

  • 三个方法的全家桶
    • hasNext()方法

取cursor与size进行对比,用来判断是否还能继续遍历下去。

当cursor=size的时候,后续方法不管是next()遍历取值还是remove()删除都因为超界不能继续,所以此时判定依然没任何意义。

    • next()方法

这个方法很简单,最终返回的即是elementData[lastRet]这个元素,即把当前的cursor对应的数据取出。值得注意的是

      • 在每次该方法执行的逻辑尾部cursor=cursor+1——使得hasNext()的遍历得以继续下去
      • 方法中引入了中间变量i,提前将cursor的值赋给i后,返值时又将i赋给lastRet——使得remove()顺利实现
    • remove()方法
      • 首先判定lastRet<0是否成立,如果成立则报错。Iterator强制我们在使用remove()方法之前要先进行next()方法为lastRet赋值,否则lastRet=-1,永远都是错误。
      • 在next()调用之后,lastRet被赋值上了cursor,调用ArrayList.this.remove(lastRet)的方法删掉对应的元素,完成删除。
      • 删除已经完成,再次将cursor=lastRet,最后lastRet=-1,为什么要设计成这样?

a、如果在遍历中删除元素,会重新创建底层数组,把旧数组的值拷贝到新的数组中,后续的元素全部进一位,这样原先next()中执行cursor=cursor+1的逻辑就乱套了,因为再次执行next()和remove()的时候我们会错过cursor位置的元素。因此,cursor也需要在每一次执行删除后得到修正,即cursor=lastRet,目的就是不要发生自增

b、这里把lastRet设为-1的目的是为了阻止程序员在没有进行next()的情况下连续去remove(),只有再次执行next()之后,lastRet完成刷新,remove()方法才知道应该删除哪个元素。

这就解释了Iterator.remove()不会出现删除元素错位的现象,即若上次执行了删除,下次删除的仍是该位置的元素。

   4、总结

  • Iterator提供了一整套精密的流程,确保整个删除、取值的逻辑正确,环环相扣,缺了每一步都可能出错。

hasNext()的目的是判定循环是否超界,next()的目的是提供向下取值,通过cursor=cursor+1相联系,两者组合即替代了传统的for循环遍历。

next()和remove()也是按照顺序去执行的,通过lastRet=cursor、lastRet=-1和lastRet=i相联系的,两者组合确保了remove()方法顺利进行。 这三个方法就像齿轮一样,一环扣一环,非常精妙的设计。

  • 可以从迭代器的角度来证明ArrayList、Vector这些继承了 AbstractList的集合是非线程安全的。

因为modCount这个成员变量存放了信息,所以不是线程安全的。试想多个线程都用迭代器去操作同一个对象,modCount发生改变,其余线程的迭代操作也会报错。

原文地址:https://www.cnblogs.com/mrpour/p/10764088.html

时间: 2024-10-30 06:39:20

ArrayList中Iterator的研究学习的相关文章

JavaSE中Collection集合框架学习笔记(3)——遍历对象的Iterator和收集对象后的排序

前言:暑期应该开始了,因为小区对面的小学这两天早上都没有像以往那样一到七八点钟就人声喧闹.车水马龙. 前两篇文章介绍了Collection框架的主要接口和常用类,例如List.Set.Queue,和ArrayList.HashSet.LinkedList等等.根据核心框架图,相信我们都已经对Collection这个JavaSE中最常用API之一有一个较为全面的认识. 这个学习过程,还可以推及到其他常用开源框架和公司项目的学习和熟悉上面.借助开发工具或说明文档,先是对项目整体有一个宏观的认识,再根

JAVA之旅(十八)——基本数据类型的对象包装类,集合框架,数据结构,Collection,ArrayList,迭代器Iterator,List的使用

JAVA之旅(十八)--基本数据类型的对象包装类,集合框架,数据结构,Collection,ArrayList,迭代器Iterator,List的使用 JAVA把完事万物都定义为对象,而我们想使用数据类型也是可以引用的 一.基本数据类型的对象包装类 左为基本数据类型,又为引用数据类型 byte Byte int Integer long Long boolean Booleab float Float double Double char Character 我们拿Integer来举例子 //整

去除ArrayList中的重复元素

ArrayList中可以存在重复元素的,若要去除重复元素必须要进行扫描,其实在原理上和数组去除重复元素是一样的. 可以利用contains方法来确定ArrayList中是否存在某个元素. 但是ArrayList中可以放任意的对象,那怎么定义各个对象是否是相同的? 可以通过自己定义类的专属equals方法,定义规则确定某两个对象是否相同,因为contains在判断某个元素是否存在ArrayList中,也是在比较这个元素时候是否和容器中存在的元素相同,若相同则存在,若不相同则不存在,contains

大杂烩 -- Java中Iterator的fast-fail分析

基础大杂烩 -- 目录 Java中的Iterator非常方便地为所有的数据源提供了一个统一的数据读取(删除)的接口,但是新手通常在使用的时候容易报如下错误ConcurrentModificationException,原因是在使用迭代器时候底层数据被修改,最常见于数据源不是线程安全的类,如HashMap & ArrayList等. 为什么要有fast-fail 一个案例 来一个新手容易犯错的例子: String[] stringArray = {"a","b"

《大数据分析中的计算智能研究现状与发展》—— 读后感

<大数据分析中的计算智能研究现状与发展>这篇文章是郭平.王可.罗阿理.薛明志发于2015年11月发表于软件学报. 该篇文章讨论了大数据分析中计算智能研究存在的问题和进一步的研究方向,阐述了数据源共享问题,并建议利用以天文学为代表的数据密集型基础科研领域的数据开展大数据分析研究.  大数据和人工智能是现代计算机技术应用的重要分支,近年来这两个领域的研究相互交叉促进,产生了很多新的方法.应用和价值.大数据和人工智能具有天然的联系,大数据的发展本身使用了许多人工智能的理论和方法,人工智能也因大数据技

Android中关于JNI 的学习(四)简单的例子,温故而知新

在第零篇文章简单地介绍了JNI编程的模式之后,后面两三篇文章,我们又针对JNI中的一些概念做了一些简单的介绍,也不知道我到底说的清楚没有,但相信很多童鞋跟我一样,在刚开始学习一个东西的时候,入门最好的方式就是一个现成的例子来参考,慢慢研究,再学习概念,再回过来研究代码,加深印象,从而开始慢慢掌握. 今天我们就再来做一个小Demo,这个例子会比前面稍微复杂一点,但是如果阅读过前面几篇文章的话,理解起来也还是很简单的.很多东西就是这样,未知的时候很可怕,理解了就很简单了. 1)我们首先定义一个Jav

JavaSE中Collection集合框架学习笔记(1)——具有索引的List

前言:因为最近要重新找工作,Collection(集合)是面试中出现频率非常高的基础考察点,所以好好恶补了一番. 复习过程中深感之前的学习不系统,而且不能再像刚毕业那样死背面试题,例如:String是固定长度的,StringBuffer和StringBuilder的长度是可以变化的.如果一旦问得深入一点,问为什么有这样的区别就傻眼了,只能一脸呆萌地看着面试官. 因此想要通过写文章的形式,系统地总结学习的内容,例如Collection架构是怎样的.有哪些相关的继承和接口实现,这样才能了解什么时候应

UnityEditor研究学习之自定义Editor

UnityEditor研究学习之自定义Editor 今天我们来研究下Unity3d中自定义Editor,这个会让物体的脚本在Inspector视窗中,产生不同的视觉效果. 什么意思,举个例子,比如游戏中我有个角色Player,他有攻击力,护甲,装备等属性. 所以我定义一个脚本:MyPlayer.cs: using UnityEngine; using System.Collections; public class MyPlayer : MonoBehaviour { public int ar

利用Mono.Cecil动态修改程序集来破解商业组件(仅用于研究学习)

原文:利用Mono.Cecil动态修改程序集来破解商业组件(仅用于研究学习) Mono.Cecil是一个强大的MSIL的注入工具,利用它可以实现动态创建程序集,也可以实现拦截器横向切入动态方法,甚至还可以修改已有的程序集,并且它支持多个运行时框架上例如:.net2.0/3.5/4.0,以及silverlight程序 官方地址:http://www.mono-project.com/Cecil 首先,我先假想有一个这样的商业组件,该组件满足了以下条件: 1. 该程序集的代码被混淆过了 2. 该程序