美文网首页
《超详细!ArrayList源码图文解析》

《超详细!ArrayList源码图文解析》

作者: 倔脾气的皮皮虾啊 | 来源:发表于2020-02-15 16:34 被阅读0次

    不诗意的女程序媛不是好厨师~
    转载请注明出处,From李诗雨---[https://blog.csdn.net/cjm2484836553/article/details/104329665]

    昨天玩了很久的arraycopy,今天让我们来看看ArrayList的源码吧。

    是的,又到了发挥我拙劣的画技的时候了~

    先预览一下本篇文章的大纲:

    在这里插入图片描述

    下面我们就开始ArrayList的源码图解之旅吧,先从增删改查讲起,然后再讲ArrayListIterator的源码,当然这之间我们还会穿插讲一下几个大坑及注意事项~

    1.增

    ArrayList添加元素的操作,涉及到2个方法 add(E object)add(int index, E object)

    1.1 add(E object) 直接在尾部添加一个元素

    add(E object)这个方法,是直接添加一个元素,是在尾部进行插入。

    如果原数组的大小不够会先进行扩容。

    最终将数据添加在尾部,同时大小加1.

    源码如下:


    在这里插入图片描述

    1.2 add(int index, E object) 在指定位置添加一个元素

    add(int index, E object)这个函数是指在 index的位置 ,插入一个新元素。

    • 我们先看插入操作的核心步骤:

      从要插入的位置开始的所有数据,都要往先后挪一位;然后再把要插入的数据放进去。

      画了个图 方便大家理解:


      在这里插入图片描述
    • 了解了插入的核心步骤之后,我们就来看看 add(int index, E object)的源码中,插入一个元素到指定位置具体是怎么实现的吧。

      一、要先检测要插入的位置index是否合法。

      二、判断数组是否够用:

      a. 如果数组够用,即 s<a.length时,直接调用arraycopy(),将从index开始的所有数据都往后挪一位。(关于arraycopy的使用我上一篇已经详细讲过了,所以这里我们应该知道他是从后往前依次往后挪一位过去的)。

      b. 如果数组满了,即s>=a.length时,这里就要

      ​ (1)先将原数组进行扩容,生成新的数组;

      ​ (2)将原数组中index之前的数据复制到新数组对应的位置中去。

      ​ (3)将原数组中index之后的数据往后挪一位的移动到新的数组中去。

      ​ (4)将新数组赋给array。

      三、将要添加的元素放在index的位置,同时有效数据个数size+1。

      源代码见下图:

      在这里插入图片描述

    补充:扩容规则

    我们都知道,数组的大小是不可变的,而ArrayList的大小是可变的。而ArrayList底层也是用到了数组,那ArrayList是如何做到大小可以动态变化的呢?

    答案就是通过扩容的方式。

    也就是Object[] newArray = new Object[newCapacity(s)];这句代码。

    现在我们来具体看看newCapacity(s)的实现:

    在这里插入图片描述
    在这里插入图片描述

    这里我们做一下解释:

    当目前的容量currentCapacity<6 时,increment=12;

    否则的话 increment等于currentCapacity的一半。

    最终返回的大小是 currentCapacity+increment

    也就是说,扩容之后要么是在原有的基础上 +12,要么就是扩大为原来的1.5倍。

    2.删

    ArrayList的删除操作,我们也来看两个 remove(Object object)remove(int index)

    2.1 remove(int index) 删除指定位置的元素

    • 同样,我们先来看看删除的核心操作:

      对应的代码就是System.arraycopy(a, index + 1, a, index, --s - index);这句代码,

      即 :如果要删除【index】位置的元素,那就要把【index】之后的所有元素都往前挪一位,覆盖掉index原本的位置.

      同样来画个图来帮助大家理解:


      在这里插入图片描述
    • 了解了核心的操作之后,我们就来看看 remove(int index) 的源代码吧:

      在这里插入图片描述

    2.2 remove(Object object) 删除某个已知元素

    remove(Object object) 删除某个已知元素。

    上面我们已经知道了删除指定位置元素的操作,那如果要删除某个已知元素的话,我们是不是也应该先找到它对应的位置,然后再进行删除呢。

    • 那问题来了,怎么找到元素对应的位置呢?

      对,通过循环遍历,并进行比较 找到对应的index.


      在这里插入图片描述

    ▲有个坑!

    ▲【注意】:

    调用remove方法, 会, 且只会 删除第一个与传入对象通过equals方法判断相等的元素。

    如果传入null, 则删除掉第一个null元素。

    所以, 如果自定义类想要使用remove方法从列表删除某个指定值对象, 还需要实现该类型自己的equals方法才行!

    ▲还有个坑!

    ArrayList是可以顺序删除节点的,但是!如果使用普通for循环,必须是从后往前删。不能从前往后删。

    我们先来看一下【错误示范】:

    ArrayList list=new ArrayList();
    list.add("a");
    list.add("b");
    list.add("c");
    
    System.out.println("删除前:"+list.toString());
    
    //顺序删除节点错误示范:从前往后删----会删不干净
    for (int i=0;i<list.size();i++){
        list.remove(i);
    }
    System.out.println("删除后:"+list.toString());
    

    【错误结果展示】:

    在这里插入图片描述

    【出错原因分析】:

    要顺序删除ArrayList的全部节点,如果我们从前往后的顺序删除,先删除【0】位置的数据,但是由于删除的时候是从后往前挪一位进行删除的,所以【0】的位置又会被下一个位置的数据覆盖上,实际上【0】还是有数据的。再画一张图来方便大家理解:


    在这里插入图片描述

    【正确的做法】:

    要想顺序删除ArrayList的所有节点,且采用普通的for循环,那只能从后往前删,这样就不会出问题。

    在这里插入图片描述

    3.改、查

    ArrayList修改数据很简单,调用的是set(int index, E object)方法,直接修改对应位置的数据即可。

    在这里插入图片描述

    ArrayList获取数据就跟简单了,由于是顺序表有下标,直接取出对应下标数据就可以了。


    在这里插入图片描述

    4.ArrayList的3种遍历方式

    ArrayList的遍历我们有三种方式:for循环增强for循环迭代器三种方式。

    (当然,增强for循环其实还是用迭代器实现的,这一点我们可以通过反编译来进行验证。)

    ArrayList arrayList = new ArrayList();
    arrayList.add("情人节");
    arrayList.add("快乐");
    arrayList.add("我");
    arrayList.add("对");
    arrayList.add("自己说~");
    
    System.out.println("for循环的方式遍历:");
    for (int i = 0; i < arrayList.size(); i++) {
        System.out.print(arrayList.get(i));
    }
    
    System.out.println();
    System.out.println("---------------------------------");
    System.out.println("增强for循环的方式遍历:");
    for (Object s : arrayList) {
        System.out.print((String) s);
    }
    
    System.out.println();
    System.out.println("---------------------------------");
    System.out.println("迭代器的方式遍历:");
    Iterator<String> iterator = arrayList.iterator();
    while (iterator.hasNext()) {
        System.out.print(iterator.next());
    }
    

    我们来看一下打印结果:


    在这里插入图片描述

    5.迭代器 ArrayListIterator源码解读

    好的,上面我们知道了ArrayList可以使用迭代器进行遍历。

    • 那为什么它可以使用迭代器这种方式呢?

      这个我们又要去看源码了,跟进arrayList.iterator()的 iterator()方法,我们会发现ArrayList有一个内部类ArrayListIterator。而 ArrayListIterator 实现了 Iterator 接口,所以才可以使用迭代器这种方式进行迭代。

    在这里插入图片描述

    现在我们就来具体看看ArrayListIterator的相关源代码和注意事项吧。

    首先,我们可以看到ArrayListIterator实现了Iterator这个接口。

    5.1 提一下什么是 Iterator (迭代器)?

    我们都知道在Java中,有很多的数据容器,这些的操作又有很多的共性。而迭代器就是给各种容器提供了公共的操作接口。这样就使得对容器的操作有了规范性。

    在Iterator接口中定义了三个方法:

    • hasNext(): 如果仍有元素可以迭代,就返回true.

    • next(): 返回迭代的下一个元素。

    • remove(): 从集合中移除返回的最后一个对象。(可选操作)

    源码如下:


    在这里插入图片描述

    5.2 ArrayListIterator中的坑▲▲▲

    ArrayListIterator 的源码其实并不难理解,就是实现了 Iterator 中的三个方法。

    但是这里有一个▲坑▲大家需要注意,那就是:

    每当我们使用迭代器遍历元素时,如果使用迭代器以外的方法修改了元素内容(如删除元素),那就会抛出ConcurrentModificationException的异常。

    让我先看一下现象,然后再从源码角度找原因。

    错误代码示例:

            ArrayList arrayList = new ArrayList();
            arrayList.add("a");
            arrayList.add("b");
            arrayList.add("c");
    
            System.out.println("移除前:" + arrayList);
            Iterator<String> iterator = arrayList.iterator();
            while (iterator.hasNext()) {
                if ("c".equals(iterator.next())) {
                    arrayList.remove("c");
                }
            }
            System.out.println("移除后:" + arrayList);
    
            //注意增强for使用的也是迭代器
            //所以一下这种操作也会报ConcurrentModificationException
            //for (Object o : arrayList) {
            //    arrayList.remove(o);
            //}
            //System.out.println("移除后2:" + arrayList);
    

    报错显示:


    在这里插入图片描述

    好的,现象我们已经看到了,那现在我们就来看看错误的原因吧。

    我们要先来了解一下这几个变量的含义:


    在这里插入图片描述

    然后我们来看一下何种情况下会报错:


    在这里插入图片描述

    先分析一下报错原因:

    在我们使用 ArrayLis 的 iterator() 方法获取到迭代器进行遍历时,会把 ArrayList 当前状态下的 modCount 赋值给 ArrayListIterator类的 expectedModCount 属性。

    如果我们在迭代过程中,使用了 ArrayList 的 remove()方法,这时 modCount 就会加 1 ,但是迭代器中的expectedModCount 并没有变化,当我们再使用迭代器的next()方法时,它就会报ConcurrentModificationException的错。

    最后我们再来比较一下 ArrayListIterator中的 remove()方法和ArrayList自己的remove()方法的不同之处,验证一下错误发生的原因:

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    ✍所以我们得到的启示是:

    每当我们使用迭代器遍历元素时,要使用迭代器自己的删除方法,而不能使用迭代器以外的方法修改了元素内容,否则会造成expectedModCount和modCount的值不一致,从而抛出ConcurrentModificationException的异常。

    此外,我们还要注意一下,增强for循环其实也是使用的迭代器,所以也要注意同样的问题。

    积累点滴,做好自己~

    相关文章

      网友评论

          本文标题:《超详细!ArrayList源码图文解析》

          本文链接:https://www.haomeiwen.com/subject/iskyfhtx.html