六千字详解!一篇看懂 ArrayList 的扩容机制(完整源码解析)

☀️今天花了很久写了这篇关于 ArrayList 扩容机制源码解析的博客,在阅读源码的过程中发现了很多之前有误解的地方,也加深了对代码的理解,所以写下了这篇博客。
🎶本文附带了流程中所有的代码和附加解析,我有信心一定能帮大家完整的梳理和认识整个流程,如果博客对你有帮助的话别忘了留下你的点赞和关注💖💖💖

文章目录

      • ArrayList 底层结构和源码分析
        • 01.整体把握
        • 02.无参构造方法
        • 03.有参构造方法
        • 04.底层扩容机制

ArrayList 底层结构和源码分析

01.整体把握

这里首先列出 ArrayList 扩容的几个特点,看完这些特点再去阅读体验会比较好

1)ArrayList 中维护了一个 Object 类型的数组,elementData。

2)当每次创建 ArrayList 对象的时候,如果使用的是无参构造器,则初始的 elementData 的容量为 0,第一次添加的时候则扩容 elementData 为 10,如果需要再次扩容,则扩容为原来的 1.5 倍

3)如果使用的是指定大小的构造器,则初始的 elementData 的容量就是指定的大小,如果需要扩容,也是直接扩容为 elementData 的 1.5 倍。


02.无参构造方法

下面来看具体的源码,首先就是 ArrayList 的无参构造方法:

    /*** Constructs an empty list with an initial capacity of ten.*/public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}

解析:可以看到,它将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给了 elementData;ArrayList 的内部实现就是基于这个 elementData 数组,它是 ArrayList 存放元素的位置,之后的拿取、扩容之类的操作本质上都是在操纵它。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个常量,来看一下它的定义:

	/*** Shared empty array instance used for default sized empty instances. We* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when* first element is added.*/private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这是一个共享的(static)空数组实例,被用作使用 默认无参构造方法 时,elementData 的默认值;它的作用是与另一个共享的空数组实例来做区分,那另一个共享数组为 EMPTY_ELEMENTDATA,它在接下来要讲的 有参构造方法 中具有很重要的作用;简单来说 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是标识着通过无参构造形成的空 ArrayList,而 EMPTY_ELEMENTDATA 是通过有参构造形成的空 ArrayList,它们在后续的扩容策略中会有所不同。

除了这个标志作用以外,它们的作用都是相同的,都是在内存中开辟了一个 共享 空间用来存放空数组,可以避免过早的为新创建的 ArrayList 实例分配内存,达到节省内存的作用。


03.有参构造方法

1)先观察 ArrayList 的有参构造方法,ArrayList 其实提供了两种有参构造方法,通过 CTRL + P 快捷键,可以查看其中的参数:

在这里插入图片描述

可以看到有参构造的第一种方式就是提供一个 int 类型的 initialCapacity,也就是初始的容量;第二种方法是提供一个集合类,构造方法会将这个集合类转为 ArrayList 的类型。

先来看指定初始容量的构造方法,这里插嘴一句,如果大家用的 idea 版本是新版的,可以多去使用那个 SmartStepInto,是调试器提供的一个智能步入的方式,可以智能的跳过一些不必要的步骤;说回到这个有参构造方法

    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);}}

如果指定的初始容量大于零,就直接构造一个容量为初始容量的 Object 数组,然后将其赋值给 elementData;否则,当初始的容量等于 0 的时候,就指定其为 EMPTY_ELEMENTDATA!这里就能看出与无参构造的区别了;如果是其他的数字,比如负数,就抛出一个异常。

2)再来看给定 Collection 的构造方法:

    public ArrayList(Collection<? extends E> c) {Object[] a = c.toArray();if ((size = a.length) != 0) {if (c.getClass() == ArrayList.class) {elementData = a;} else {elementData = Arrays.copyOf(a, size, Object[].class);}} else {// replace with empty array.elementData = EMPTY_ELEMENTDATA;}}

解析:首先通过 Collction 接口中定义的 toArray() 方法,将集合类转为一个数组,然后指定 size(当前 ArrayList 实例中存放元素的个数)赋值成数组的长度,然后判断这个长度是否为 0,如果不为 0 就将其赋值给 elementData。

但是关于第二个 else 我其实是有些疑惑的,我猜测写这段 else 逻辑 elementData = Arrays.copyOf(a, size, Object[].class); 是为了保证 ArrayList 中始终维护的是一个 Object 数组,但是其实第一个语句 Object[] a = c.toArray(); 已经确定了这是 Object 数组,所以单从这里看其实是有些冗余的,如果大家有什么见解可以在评论区指教一下。

如果长度为 0 的话,就将 elementData 赋值为 EMPTY_ELEMENTDATA,所以这个 static 属性其实就是标识有参构造方法形成的空 elementData 数组。


04.底层扩容机制

终于到了重中之重的扩容机制,也是面试题中经常会问到的部分,直接来追一下源代码:

	/*** Appends the specified element to the end of this list.** @param e element to be appended to this list* @return <tt>true</tt> (as specified by {@link Collection#add})*/public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;}

add 的代码其实比较简单,首先就是调用了 ensureCapacityInternal() 来保证存储空间(elementData)足够放下这个新的元素,然后再将元素放入其中:elementData[size++] = e;,那接下来要看什么呢?不用多说,肯定是这个 ensureCapacityInternal() 方法。

    private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}

在这个方法中,首先是使用了 calculateCapacity(elementData, minCapacity) 方法去计算容量,这个 minCapacity 是上面传过来的,不管是上面的有参构造还是无参构造,最终形成的 ArrayList 实例的长度都是 0,所以此时的 minCapacity 就是 1。

下面的是 calculateCapacity() 方法

    private static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}

首先看第一个 if 语句 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),先去判断了这个 elementData 是否为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那什么时候会是这个元素呢?答案就是 无参默认构造方法 创建的实例,这时候就发挥它的作用了,当发现 ArrayList 实例是通过无参构造形成的,就会去取 minCapacity 和 DEFAULT_CAPACITY 中的最小值,而 DEFAULT_CAPACITY 它的值就是 10,这也就是为什么很多面试题的答案说,首先创造空数组,然后第一次扩容的时候扩容成 10;但这并不是完全正确的,当不是无参构造的时候,其实此时的 minCapacity 仍然是 1。

OK,得到了 minCapacity,我们回到上一个方法:

	private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}

然后就是取执行 ensureExplicitCapacity() 方法了:

	private void ensureExplicitCapacity(int minCapacity) {modCount++;// overflow-conscious codeif (minCapacity - elementData.length > 0)grow(minCapacity);}

首先是对这个 modCount 做了一个自增,这个变量记录了这个集合扩容的次数,然后去判断 minCapacity - elementData.length > 0 也就是最小需要的长度能否通过当前的 elementData 长度满足,如果不能就进入扩容方法 grow(),并且传入 minCapacity。

private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1);if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win:elementData = Arrays.copyOf(elementData, newCapacity);}

首先记录下 oldCapacity,也就是扩容之前 elementData 的长度,然后执行这条语句 newCapacity = oldCapacity + (oldCapacity >> 1);,使用了右移运算符,右移运算符其实就可以看作除以 2 的操作,然后再加上原本的 oldCapacity,最终就是原本长度的 1.5 倍,但是因为是最后将其转换为了 int,所以其扩容效果是 小于等于 1.5 倍的;然后后面就是将此时的 minCapacity 和这个由原本长度拓展 1.5 倍的长度做一个对比,取最大,后一个 if 语句是为了处理扩容过限的问题,代码比较容易,最后贴给大家看一下。

最终就是调用 Arrays.copyOf(elementData, newCapacity); 将原本 elementData 中的内容移动到新拓展的,长度为 newCapacity 的数组中,这就完成了一个完整的扩容。

  • 此时如果是无参构造,它带进来的 minCapacity 就是 10,最终其会被拓展为 10
  • 如果是有参构造的话,带进来的 minCapacity 其实就是 1,且计算得 int newCapacity = oldCapacity + (oldCapacity >> 1); 结果是 0,那最终 elementData 会被拓展成 1。

所以说第一次拓展均拓展成 10 其实是不准确的;其他长度的拓展大家顺着流程推导一下就很容易得到了。

最后贴上 hugeCapacity() 方法的源码:

	private static int hugeCapacity(int minCapacity) {if (minCapacity < 0) // overflowthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}

这个方法是为了处理 newCapacity 的长度超过定义的数组的最大长度(MAX_ARRAY_SIZE,它被定义为 Integer.MAX_VALUE - 8),此时就使用 minCapcaity 进行初始化,如果发现 minCapacity < 0,就大概率是因为越界导致的了,因为当 int 超过 231 - 1 的时候,就会因为错位变成负数,所以此时抛出 OutOfMemoryError 超过内存限制错误,然后判断此时的 minCapacity 是否大于 MAX_ARRAY_SIZE,如果不大于就赋值成它,否则赋值成 Integer.MAX_VALUE,也就是 int 的最大值,如果还是不够会在后面因为越界抛出异常的。

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

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

相关文章

五款常用在线JavaScript加密混淆工具详解:jscrambler、JShaman、jsfack、ipaguard和jjencode

摘要 本篇技术博客将介绍五款常用且好用的在线JavaScript加密混淆工具&#xff0c;包括 jscrambler、JShaman、jsfack、freejsobfuscator 和 jjencode。通过对这些工具的功能及使用方法进行详细解析&#xff0c;帮助开发人员更好地保护和加密其 JavaScript 代码&#xff0c;提…

STM32之HAL开发——串口配置(CubeMX)

串口引脚初始化&#xff08;CubeMX&#xff09; 选择RCC时钟来源 选择时钟频率&#xff0c;配置为最高频率72MHZ 将单片机调试模式打开 SW模式 选择窗口一配置为异步通信模式 点击IO口设置页面&#xff0c;可以看到当前使用的串口一的引脚。如果想使用复用功能&#xff0c;只需…

原型链-(前端面试 2024 版)

来讲一讲原型链 原型链只存在于函数之中 四个规则 1、引用类型&#xff0c;都具有对象特性&#xff0c;即可自由扩展属性。 2、引用类型&#xff0c;都有一个隐式原型 __proto__ 属性&#xff0c;属性值是一个普通的对象。 3、引用类型&#xff0c;隐式原型 __proto__ 的属…

Vue——案例01(查询用户)

一、案例实现页面 二、案例实现效果 1. 查询效果 2. 年龄升序 3. 年龄降序 4. 原顺序 三、案例实现思路 1. 定义界面所需标签样式 <div id"app"><h2>查询用户:</h2><input type"text" placeholder"请输入名字"/><b…

代码随想录算法训练营第35天| 435. 无重叠区间、763.划分字母区间、56. 合并区间

435. 无重叠区间 题目链接&#xff1a;无重叠区间 题目描述&#xff1a;给定一个区间的集合 intervals &#xff0c;其中 intervals[i] [starti, endi] 。返回 需要移除区间的最小数量&#xff0c;使剩余区间互不重叠 。 解题思想&#xff1a; 这道题目和射气球很像。 *“需…

rust使用Command库调用cmd命令或者shell命令,并支持多个参数和指定文件夹目录

想要在不同的平台上运行flutter doctor命令&#xff0c;就需要知道对应的平台是windows还是linux&#xff0c;如果是windows就需要调用cmd命令&#xff0c;如果是linux平台&#xff0c;就需要调用sh命令&#xff0c;所以可以通过cfg!实现不同平台的判断&#xff0c;然后调用不同…

代码随想录算法训练营第二十四天|77.组合、216.组合Ⅲ

文档链接&#xff1a;https://programmercarl.com/ LeetCode77.组合 题目链接&#xff1a;https://leetcode.cn/problems/combinations/ 思路&#xff1a; 回溯三部曲&#xff1a; 第一步&#xff1a;确定函数返回值和参数类型 第二步&#xff1a;确定终止条件 第三步&a…

JUC/多线程的基本使用(一)

一、基本使用 Thread、Runnable、FutureTask Java多线程-CSDN博客https://blog.csdn.net/m0_71534259/article/details/132381495?spm1001.2014.3001.5501 二、查看进程线程的方法 windows 任务管理器可以查看进程和线程数&#xff0c;也可以用来杀死进程 tasklist 查看…

ICLR2024:南洋理工发布!改几个参数就为大模型注入后门

随着大语言模型&#xff08;LLMs&#xff09;在处理自然语言处理&#xff08;NLP&#xff09;相关任务中的广泛应用&#xff0c;它们在人们日常生活中的作用日益凸显。例如&#xff0c;ChatGPT等模型已被用于各种文本生成、分类和情感分析任务。然而&#xff0c;这些模型潜在的…

上位机图像处理和嵌入式模块部署(qmacvisual非opencv算法编写)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 我们都知道&#xff0c;qmacvisual本身依赖于qtopencv的实现。大部分的界面都是依赖于qt的实现。图像算法部分&#xff0c;也是大部分都依赖于open…

RockChip Android8.1 Settings

一:Settings一级菜单 1、AndroidManifest.xml 每个APP对应都有一个AndroidManifest.xml,从该文件入手分析最为合适。 packages/apps/Settings/AndroidManifest.xml 根据<category android:name="android.intent.category.LAUNCHER" />可找到当前当前APP a…

【计算机网络篇】数据链路层(4.2)可靠传输的实现机制

文章目录 &#x1f354;可靠传输的实现机制⭐停止 - 等待协议&#x1f5d2;️注意 &#x1f50e;停止 - 等待协议的信道利用率&#x1f5c3;️练习题 ⭐回退N帧协议&#x1f388;回退N帧协议的基本工作流程&#x1f50e;无传输差错的情况&#x1f50e;超时重传的情况&#x1f5…