集合类源码浅析の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 codeif (minCapacity - elementData.length > 0)grow(minCapacity);}

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


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

    private void grow(int minCapacity) {//0int oldCapacity = elementData.length;//0int newCapacity = oldCapacity + (oldCapacity >> 1);//0-10 = -10 <0if (newCapacity - minCapacity < 0)//10newCapacity = 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 codeif (minCapacity - elementData.length > 0)grow(minCapacity);}

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

  private void grow(int minCapacity) {//10int oldCapacity = elementData.length;//10 + 10 / 2 = 15int 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 workreturn 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.mzph.cn/news/846684.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

240602-通过命令行实现HuggingFace文件上传

A. 登录显示 A.1 MacOS A.2 Windows B. 操作步骤 B.1 操作细节 要通过命令行将文件上传到 Hugging Face&#xff0c;可以使用 huggingface-cli 工具。以下是详细步骤&#xff1a; 安装 huggingface_hub 包&#xff1a; 首先&#xff0c;确保已经安装了 huggingface_hub 包。可…

基于springboot实现青年公寓服务平台系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现青年公寓服务平台系统演示 摘要 传统信息的管理大部分依赖于管理人员的手工登记与管理&#xff0c;然而&#xff0c;随着近些年信息技术的迅猛发展&#xff0c;让许多比较老套的信息管理模式进行了更新迭代&#xff0c;房屋信息因为其管理内容繁杂&#xff…

“人工智能AI+” 应用场景盘点

在这个科技与梦想交相辉映的时代&#xff0c;人工智能已不再停留于遥不可及的概念构想&#xff0c;而是化身为一股汹涌的创新洪流&#xff0c;深刻塑造着社会的每一个角落。从文化艺术的智慧火花到生命科学的精密探索&#xff0c;从工业制造的革新升级到日常生活的细致入微&…

Delphi使用TMS.MQTT开发Mqtt客户端

服务端用的是 mosquitto ,下载地址Download | Eclipse Mosquitto 安装完成后需要配置 找到安装目录:mosquitto.conf,打开后修改 allow_anonymous false(禁止匿名登录),password_file D:\Program Files\mosquitto\pwfile.example(密码存放位置) 创建新用户,安装目录…

Least-Squares Rigid Motion Using SVD——文献精读(使用 SVD 方法求解 ICP 问题)

一、文章信息与摘要 文章标题&#xff1a;Least-Squares Rigid Motion Using SVD&#xff08;使用奇异值分解的最小二乘刚性运动&#xff09; 说明本文的核心目标&#xff1a;计算对齐两组对应点的最佳拟合刚性变换的步骤 二、问题描述 假设P{p1,p2,...,pn}和Q{q1,q2,...,qn…

Feature Manipulation for DDPM based Change Detection

基于去噪扩散模型的特征操作变化检测 文章提出了一种基于去噪扩散概率模型&#xff08;DDPM&#xff09;的特征操作变化检测方法。变化检测是计算机视觉中的经典任务&#xff0c;涉及分析不同时间捕获的图像对&#xff0c;以识别场景中的重要变化。现有基于扩散模型的方法主要…

C++ 习题精选(2)

目录 1. 验证回文串2. 字符串相乘 1. 验证回文串 题目描述&#xff1a;如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。字母和数字都属于字母数字字符。给你一个字符串 s&#xff…

Vue01-vue的简介

一、Vue是什么&#xff1f; 一套用于构建用户界面的渐进式javaScript框架。 构建用户界面&#xff1a; 渐进式&#xff1a; 目前Vue的地位&#xff1a;生态完善&#xff0c;国内前端工程师必备技能。 二、Vue的特点 一个XXX.vue就是一个组件&#xff0c;封装的概念&#xff0c…

threejs的三维前端项目用THREEPlaneGeometry创建平面几何体对象的方法

在 Three.js 中&#xff0c;可以使用THREE.PlaneGeometry类来创建平面几何体对象。THREE.PlaneGeometry类用于创建一个具有指定宽度和高度的平面几何体。 艾斯视觉作为行业ui设计与前端开发服务商很高兴能在这里与你共同探讨&#xff1a;以下是一个示例代码&#xff0c;展示了如…

文心一言 VS 讯飞星火 VS chatgpt (272)-- 算法导论20.2 7题

七、试说明如果 PROTO-vEB-MINIMUM 中的第 9 行被执行&#xff0c;则proto-vEB 结构为空。如果要写代码&#xff0c;请用go语言。 文心一言&#xff1a; 在 PROTO-vEB-MINIMUM 算法中&#xff0c;我们假设你指的是一个基于位向量&#xff08;bit-vector&#xff09;的扩展二进…

OSG学习记录

学习自OpenSceneGraph Quick Start Guide的中文版&#xff0c;建议学习书&#xff08;比较全面&#xff09; OSG的内存管理机制 程序保存一个指向根节点的指针&#xff0c;不保存场景图形中其他节点的指针。根节点将直接或间接地“引用”场景图形中的所有的节点。 当应用程序…

回溯算法 -- 77. 组合

目录 一.题目描述 二.解题思路 三.回溯三部曲 3.1确定递归函数的返回值以及参数 3.2回溯算法的终止条件 3.3确定单层循环搜索逻辑 四.具体的代码 一.题目描述 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案…

【Linux】进程间通信(System V IPC)

这节我们开始学习System V IPC方案。 分别是共享内存&#xff0c;消息队列与信号量 会着重讲解共享内存&#xff0c;但是消息队列与信号量只会说明一下原理。 原因&#xff1a;System V是新设计的一套标准 与文件的整合度不高只能进行本地通信 更何况&#xff0c;我们现在有…

若依开发-数据库修改密码

若依忘记密码 在SecurityUtils类添加 public static void main(String[] args) {System.out.println(SecurityUtils.encryptPassword("admin123"));}即可打印出加密后密码

探索安全之道 | 企业漏洞管理:从理念到行动

如今&#xff0c;网络安全已经成为了企业管理中不可或缺的一部分&#xff0c;而漏洞管理则是网络安全的重中之重。那么企业应该如何做好漏洞管理呢&#xff1f;不妨从业界标准到企业实践来一探究竟&#xff01;通过对业界标准的深入了解&#xff0c;企业可以建立起完善的漏洞管…

谷歌发布文生视频模型——Veo,可生成超过一分钟高质量1080p视频

前期我们介绍过OpenAI的文生视频大模型-Sora 模型&#xff0c;其模型一经发布&#xff0c;便得到了大家疯狂的追捧。而Google最近也发布了自己的文生视频大模型Veo&#xff0c;势必要与OpenAI进行一个正面交锋。 Veo 是Google迄今为止最强大的视频生成模型。它可以生成超过一分…

JVM虚拟机性能监控工具

命令行工具 jps 虚拟机进程状况查询工具 jps(JVM Process Status Tool)&#xff0c;可以列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执行主类名称或者jar文件名&#xff0c;还有这些进程的本地虚拟机唯一ID(LVMID&#xff0c;Local Virtual Machine Identifier)。 # …

网页安全登陆的设计思路

对于Web网站来讲,不管是企业内容信息化系统,还是公共站点(博客、音视频站等),都有需要用户注册和登录的功能。用以识别用户、信息交互、信息隔离以及商业行为等场景。用户数据已成为网站的重要资产。保护用户信息(数据)是网站安全运行的关键任务。本文以用户安全登录的场…

521源码-网站源码-Thinkphp聊天室H5实时聊天室群聊聊天室自动分配账户完群组/私聊/禁言等功能/全开源运营版本

全开源运营版本聊天室H5实时聊天室群聊聊天室自动分配账户完群组/私聊/禁言等功能 都是去年买的&#xff0c;很多买的源码基本都下架了&#xff0c;详情还是套已经老站的&#xff0c;可能网上已经流传了点&#xff0c;不过还是不影响这个源码的牛逼所在 运营版本的聊天室&…

JVM之【运行时数据区2——堆】

三、堆&#xff08;Heap&#xff09; 1、什么是堆 在Java虚拟机&#xff08;JVM&#xff09;中&#xff0c;堆&#xff08;Heap&#xff09;是用于动态分配内存的区域。在Java程序运行时&#xff0c;所有对象和数组都是在堆中分配内存的。堆是Java内存模型的重要组成部分&…