集合类源码浅析のArrayList

源码分析路线图:

初级部分:ArrayList->LinkedList->Vector->HashMap(红黑树数据结构,如何翻转,变色,手写红黑树)->ConcurrentHashMap

中级部分:Spring->Spring MVC->Spring Boot->Mybatis核心类源码

高级部分:中间件源码(有生之年系列)


第一篇,从最简单的ArrayList入手分析

1、成员变量

        集合的初始容量:

private static final int DEFAULT_CAPACITY = 10;

        下面两个成员变量都是Object类型的空数组,区分在于变量名,是用于区别通过何种构造方法创建了ArrayList集合,后面会提到。

private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

        elementData用于存放集合中元素的数组(ArrayList的底层本质上也是数组)

        为什么要用transient修饰?我们首先简单复习一下transient关键字的作用:

        字段声明为 transient表示该字段不会被序列化,即在对象被序列化为字节流时,transient字段的值不会被包含在序列化结果中。在对象反序列化后,elementData 数组将恢复为 null。

transient Object[] elementData;

        记录当前集合的大小

private int size;

2、构造方法

        2.1、无参构造

        将空数组赋值给成员变量的elementData:

   public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
        2.2、有参构造一

        参数部分:

  • int initialCapacity:数组的大小,范围从0到Integer的最大值。

        使用该构造方法,会传递一个初始数组的大小,然后进行if判断。

  • 分支一:传入的参数大于0,就创建一个长度为参数的空数组,赋值给成员变量的elementData。
  • 分支二:传入的参数为0,就将空数组赋值给成员变量的elementData。
  • 分支三:传入的参数小于0,抛出异常,数组的长度不可能为负数。
   public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

        通过上面两种构造的分析,可以得出一个结论:成员变量EMPTY_ELEMENTDATA用于区分用户使用的是有参构造,但是传递的参数为0。DEFAULTCAPACITY_EMPTY_ELEMENTDATA代表用户使用的是无参构造。

        2.3、有参构造二

         参数部分:

  • Collection<? extends E> c:Collection及其子类集合。

        首先会将传入的集合转换为数组并赋值给成员变量elementData。

        然后进入条件判断:

  • 分支一:将传入的集合转换为数组的长度赋值给成员变量size,如果不为0,就再次进入判断,检查 elementData 的实际类型是否为 Object[]。如果不是,就将其复制为一个新的 Object[] 类型的数组,并将其赋值给 elementData。
  • 分支二:将空数组赋值给成员变量的elementData。
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

3、add方法

        重点介绍两个重载的方法:方法一是将一个元素放入链表的末尾,方法二是将元素放入指定的下标:

        3.1、add(E e)

        首先会跳转到ensureCapacityInternal(size + 1); 方法:

        分支一:

        假设目前是通过无参构造或有参构造传递0实例化的ArrayList,此时的size应该为0,size+1=0+1 = 1。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

        条件块满足,取得传入参数(1)和DEFAULT_CAPACITY(10)的最大值,赋值给参数minCapacity,然后再次跳转入ensureExplicitCapacity(minCapacity);方法:

   private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

        modCount++;是其父类AbstractList 中的成员变量,用于记录并发修改次数(不能一边遍历集合一边增删元素,否则会抛出并发修改异常。如果需要,请使用迭代器遍历)


        然后会进入条件块。10-0>0,进入最关键的grow(minCapacity); 扩容方法,传入参数10:

    private void grow(int minCapacity) {
        //0
        int oldCapacity = elementData.length;
        //0
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //0-10 = -10 <0
        if (newCapacity - minCapacity < 0)
            //10
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        //扩容成一个长度为10的新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

        扩容使用了Arrays.copyOf(elementData, newCapacity); 方法,将原有数组中的元素复制到一个长度为10的新数组中,然后重新赋值给elementData。(也就是此时的elementData是一个长度为10的空数组)

        然后回到add(E e)方法的elementData[size++] = e; 这一行,将元素赋值给elementData的第0索引的元素,然后size+1。(数组的长度为1,元素在0索引上,复习一下,数组的最大下标等于长度-1)

        上述过程,证明了ArrayList的扩容时机是在加入第一个元素前进行扩容,然后才会加入元素。


        分支二:

        假设目前集合中已经有了10个元素,现在调用add(E e)添加第11个元素:

        同样首先进入ensureCapacityInternal(int minCapacity)方法,参数为size + 1 = 10 + 1 = 11。

    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

         这时的条件就不满足了,直接进入ensureExplicitCapacity(minCapacity) 方法:

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

         11 - 10 = 1 > 0,条件块满足,进入grow(minCapacity); 扩容方法,传入参数11:

  private void grow(int minCapacity) {
        //10
        int oldCapacity = elementData.length;
        //10 + 10 / 2 = 15
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //15 - 10 = 5 > 0 条件不满足
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //条件也不满足
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        //扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

        扩容同样使用Arrays.copyOf(elementData, newCapacity); 方法,将原有数组中的元素复制到一个长度为15的新数组中,然后重新赋值给elementData。(也就是此时的elementData是一个长度为15的数组。注意,此时数组中还是只有10个元素,最新的一个仍未添加

        然后回到add(E e)方法的elementData[size++] = e; 这一行,将元素赋值给elementData的第10个索引的元素,然后size+1。

        上述过程,证明了ArrayList的扩容机制是,首次添加元素前扩容为10,以后都是扩容为旧容量的1.5倍。

        并且元素是放在链表的末尾。

        最后值得一提的是,ArrayList并不是无限制扩容,最大容量为Integer的长度。详见hugeCapacity(int minCapacity) 方法,逻辑很简单,有兴趣的请自己研究下!

        3.2、add(int index, E element)

        这个方法的意思是将元素加到指定的索引上。

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

        rangeCheckForAdd(index)方法用于检查数组下标越界异常。

        ensureCapacityInternal(size + 1) 方法用于判断是否扩容,并执行扩容逻辑,不再重复说明。

        System.arraycopy(elementData, index, elementData, index + 1,size - index) 是实现将元素添加到指定索引的前置工作,将 elementData 数组中从索引 index 开始到末尾的元素向右移动一个位置,为在索引 index 处插入新元素腾出空间。

        然后将元素加到index所在的索引上,并且size长度+1。

        此方法涉及到数组元素的移动,所以效率较低!

4、remove方法

        重点介绍两个remove方法,方法一是删除指定索引的元素,方法二是删除指定的元素。

        4.1、remove(int index)

        此方法是删除传递参数所在索引的元素。

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

        既然传递进来的是索引,就必须进行下标合法性的检查,通过rangeCheck(index) 方法。

        E oldValue = elementData(index); 方法的作用是返回index索引位置的元素。

        int numMoved = size - index - 1; 假设目前数组的长度为3,需要删除1索引处的元素,计算得到的值就是3 - 1 - 1 = 1。

        System.arraycopy(elementData, index+1, elementData, index,numMoved) 将 elementData 数组中从索引 index+1 开始的 numMoved 个元素向左移动一个位置,以覆盖索引 index 处的元素。

        为了方便理解我们来画个图:

        初始情况:

         执行System.arraycopy(elementData, index+1, elementData, index,numMoved) 代码,将要删除元素的索引是1:

        执行完上面的代码后,将即将删除元素所在索引的后面元素向前移动,覆盖掉删除的元素。

        elementData[--size] = null; 然后将链表末尾的元素的指针指向null,方便垃圾回收。

        最后返回被删除的元素。

        由此可见,ArrayList指定索引删除的效率不高,因为和指定索引新增一样,也涉及到其余元素的移动,如果元素较多则速度较慢。

        4.2、remove(Object o)

        此方法是删除指定的元素

  • 分支一:传递的参数为null,则从0索引开始遍历整个集合,如果某个索引下的元素为null,就调用fastRemove(index)方法删除对应索引的元素。
  • 分支二:传递的参数不为null,则从0索引开始遍历整个集合,如果某个索引下的元素和传入的元素相等,就调用fastRemove(index)方法删除对应索引的元素。
    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;
    }

        可以看出,remove(Object o)  的本质依旧是遍历集合,删除指定索引的元素,但是利用的是fastRemove(index)方法:

    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; // clear to let GC do its work
    }

        虽然名字叫fast,实际上依旧涉及到数组元素下标的移动,所以效率依旧不高。

  5、并发修改异常原因分析

        有这样一段代码,通过增强for循环一边遍历一边增删元素:

public class Test {
    public static void main(String[] args) {
        ArrayList<String> strings = new ArrayList<>();
        strings.add("a");
        strings.add("b");
        strings.add("c");

        for (String s : strings) {
            if (s.equals("a")){
                strings.remove(s);
            }
        }
    }
}

        毫无悬念的出现了并发修改异常:

        我们来跟踪一下堆栈信息,这个异常出现在ArrayList的私有内部类Itr中的next()方法中的checkForComodification()

        Itr实现了迭代器接口,成员变量expectedModCount的值是ArrayList 父抽象类的成员变量modCount


        首先通过打断点的方式了解一下modCount的机制,在Test的第7行打上断点,以及启动程序后,在AbstractList 类的modCount成员变量上打上断点,方便查看不同操作时modCount值的变化情况。

         当我们添加第一个元素时,modCount+1 = 1

         后续每添加一个元素,modCount都会+1,最终所有元素添加完成后,modCount = 3。

         然后进入for循环的if块,删除a元素:

        底层调用的fastRemove()方法,modCount = 3 + 1 = 4

        在进入下一次循环时,Itr的成员变量expectedModCount为3

        而实际modCount = 3 + 1 = 4 ,所以在checkForComodification() 中抛出异常。

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

总结

        ArrayList是线程不安全的集合,一边遍历一边增删元素会导致并发修改异常。它的底层实现是数组,在构造时,可以自定义集合的长度,如果没有定义,则在添加第一个元素前扩容长度为10,然后会添加元素,后续扩容量为原有容量的1.5倍。插入和删除都可以指定下标位置,增删的效率较低,因为无论何种方式都涉及到数组元素的移动。如果没有指定下标,新增的元素默认在集合的尾部。相对的查询效率较高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/670744.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一文彻底讲透 PyTorch

节前&#xff0c;我们组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、今年参加社招和校招面试的同学。 针对大模型技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备面试攻略、面试常考点等热门话题进行了深入的讨论。 汇总合集…

linux系统的vscode快捷键大全

多行注释快捷键&#xff1a;ctrl shift A 单行注释&#xff1a;ctrl K ctrl C 取消单行注释&#xff1a;ctrl K ctrl U

Nvidia Jetson/Orin +FPGA+AI大算力边缘计算盒子:轨道交通监控系统

株洲中车时代电气股份有限公司&#xff08;下称中车时代电气&#xff09;是中国中车旗下股份制企业&#xff0c;其前身及母公司——中车株洲电力机车研究所有限公司创立于1959年。中车时代电气扎根株洲&#xff0c;走好两条钢轨&#xff0c;走出两条钢轨。中车时代电气秉承“双…

抽象一个通用的配置冲突解决方案

最近的开发项目中遇到了一个关于配置冲突的解决和产品设计&#xff0c;一直以来都没有处理好。最近抽空整理了一下思路和设计&#xff0c;并做了抽象&#xff0c;后续的类似使用&#xff0c;可以做到直接复用。 思路和代码见&#xff1a;github地址&#xff1a;https://github…

RTA GMH系列 SERIE MOTION电机驱动板手侧 英文版

RTA GMH系列 SERIE MOTION电机驱动板手侧 英文版

ESP-01S 使用 arduino 烧录程序

一、设置 arduino 编辑器 1、文件-首选项-附加开发版管理网址中添加 http://arduino.esp8266.com/stable/package_esp8266com_index.json 2、工具-开发板管理 搜索 8266 并下载 ) 3、工具-开发板 在 8266 里面选择 Generic ESP8266 Module 4、工具-端口 记得选择对应的端口 …

Pytorch的学习

1.基本数据&#xff1a;Tensor Tensor&#xff0c;即张量&#xff0c;是PyTorch中的基本操作对象&#xff0c;可以看做是包含单一数据类型元素的多维矩阵。从使用角度来看&#xff0c;Tensor与NumPy的ndarrays非常类似&#xff0c;相互之间也可以自由转换&#xff0c;只不过Te…

简单的基于小波分解和独立分量分析的脑电信号降噪(Python)

脑电信号是一种典型的非平稳随机信号且存在一定的非高斯性和非线性。传统的分析处理方法是将脑电信号近似看做线性、准平稳、高斯分布的随机信号&#xff0c;这使得分析结果往往不能令人满意&#xff0c;实用性较差。现代的小波变换方法和独立分量分析方法的提出为有效地分析脑…

LeetCode---字符串

344. 反转字符串 编写一个函数&#xff0c;其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间&#xff0c;你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 代码示例&#xff1a; //时间复杂度: O(n) //空间…

职场思考-在行业坚守中实现个人增值(13)

滚石不生苔&#xff0c;转行不聚财 在自己工作几年后&#xff0c;职业竞争力会由专业能力向行业经验进行转化 如果你不具备足够的行业积累&#xff0c;即使在某个专业上有足够的能力&#xff0c;你也难以得到待遇或职位的提升&#xff0c;陷入高不成低不就的局面 掌握完成岗位工…

使用pikachu Xss后台出现的问题

在进行xss-x漏洞实验的时候&#xff0c;一直出现上述错误&#xff0c;查找了很多&#xff0c;终于找到问题所在 pikachu使用的数据库为同一个数据库&#xff0c;千万别被pkxss误导&#xff0c;以为pikachu还有一个数据库为pkxss,所以在配置的时候写下如下图的

Python知识点5---字符串的使用

提前说一点&#xff1a;如果你是专注于Python开发&#xff0c;那么本系列知识点只是带你入个门再详细的开发点就要去看其他资料了&#xff0c;而如果你和作者一样只是操作其他技术的Python API那就足够了。 Python的字符串在使用上和其他语言的差别不大&#xff0c;常规操作都…

Nginx实战:nginx支持带下划线的header

nginx对header 的名字字符做了限制&#xff0c;默认 underscores_in_headers 为off&#xff0c;表示如果header name中包含下划线&#xff0c;则忽略掉&#xff0c;后端服务就获取不到该请求头。 为了支持header带下划线的参数&#xff0c;可以在http内或者server内设置如下参数…

FreeRTOS基础(七):临界段代码保护及调度器挂起与恢复

上一篇博客我们详细介绍了FreeRTOS是怎么管理中断的&#xff0c;其实&#xff0c;从本质上来讲就是将就是利用的BASEPRI这个寄存器&#xff0c;来屏蔽优先级低于某一个阈值的中断&#xff0c;当设置为0的时候&#xff0c;就是打开所有中断&#xff0c;所有中断都可以响应。这样…

【VMware虚拟机中ubuntu系列】—— 在虚拟机中使用本机摄像头的详细教程与常见问题分析及解决

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、虚拟机调用本机摄像头(1) 启动VMware USB 服务(2) 连接本机摄像头(3) 测试摄像头的连接 二、安装usb驱动二、运行usb_cam.launch时出现select timeout的报错…

希捷硬盘怎么恢复数据? 5 个免费希捷数据恢复软件

希捷已迅速成为全球最大的数字存储提供商。许多人选择并使用希捷外置硬盘来存储他们的媒体文件、学校或工作文件以及其他重要数据。有时&#xff0c;希捷硬盘中的数据会丢失。 如果您丢失了希捷硬盘上的数据&#xff0c;请不要惊慌。在专业的希捷数据恢复软件的帮助下&#xf…

【c++进阶(一)】STL之string接口介绍

&#x1f493;博主CSDN主页:Am心若依旧&#x1f493; ⏩专栏分类c从入门到精通⏪ &#x1f69a;代码仓库:青酒余成&#x1f69a; &#x1f339;关注我&#x1faf5;带你学习更多c   &#x1f51d;&#x1f51d; 1.前言 本章重点 本章着重讲解string中一些重要的接口函数&…

SOUI Combobox 实现半透明弹出下拉框

SOUI默认情况下combobox的弹出框不是半透明的&#xff0c;这个时候如果背景透明时&#xff0c;滚动条会出现黑色背景&#xff0c;这个时候只需要在在combobox下添加一个子节点 <dropdownStyle translucent"1"></dropdownStyle> 这样一个窗口默认即实现…

Nature Communications|柔性自驱动仿生眼(离子凝胶/仿生眼/柔性电子)

2024年4月10日,黄维(Wei Huang)院士、南京工业大学刘举庆(Juqing Liu)教授和刘正东(Zhengdong Liu)副教授课题组,在《Nature Communications》上发布了一篇题为“A bionic self-driven retinomorphic eye with ionogel photosynaptic retina”的论文,罗旭(Xu Luo)、陈晨(…

vscode过滤器@modified(查看配置了哪些设置)

文档 visualstudio•docs•getstarted•settingshttps://code.visualstudio.com/docs/getstarted/settings 说明 使用modified可以过滤出&#xff1a; 配置过的设置&#xff08;和默认值不同&#xff09;&#xff1b; 在 settings.json 文件中配置了值的设置 步骤 1.打开…