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

news2024/10/10 14:25:25

☀️今天花了很久写了这篇关于 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 code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

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

private void grow(int minCapacity) {
        // overflow-conscious code
        int 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) // overflow
            throw 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.coloradmin.cn/o/1554231.html

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

相关文章

网络安全笔记-day8,DHCP部署

DHCP部署与安全 全称&#xff08;Dynamic Host Configura Protocol&#xff09;动态主机配置协议 DHCP原理 DHCP协议_科来测试dhcp网络包-CSDN博客&#x1f50d; 注意的是利用广播地址发送包 ACK&#xff08;确认&#xff09; 如果DHCP服务器损坏&#xff0c;则在87.5%时…

Python基础:标准库 -- pprint (数据美化输出)

1. pprint 库 官方文档 pprint --- 数据美化输出 — Python 3.12.2 文档 pprint — Data pretty printer — Python 3.12.2 documentation 2. 背景 处理JSON文件或复杂的嵌套数据时&#xff0c;使用普通的 print() 函数可能不足以有效地探索数据或调试应用程序。下面通过一…

网络服务练习题

综合练习&#xff1a;请给 openlab 搭建 web 网站 网站需求&#xff1a; 1. 基于域名 www.openlab.com 可以访问网站内容为 welcome to openlab!!! 2. 给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料 和缴费网站&#xff0c;基于&#xff0c; www.openlab.c…

stm32定时器中断函数回调函数

方式一&#xff1a;stm32定时器中断可以直接在硬件中断函数TIM3_IRQHandler执行。 在HAL库中可以注册回调函数&#xff0c;在定时器中断发生时调用注册的函数&#xff0c;这样可以统一接口&#xff0c;大大提高函数可读性&#xff0c;和硬件解耦提高程序可移植性。 使用过程如…

【JVM】Java类加载器 和 双亲委派机制

1、java类加载器的分类 JDK8及之前 启动类加载器&#xff0c;BootStrap Class Loader,加载核心类,加载jre/lib目录下的类&#xff0c;C实现的拓展类加载器&#xff0c; Extension Class Loader&#xff0c;加载java拓展类库&#xff0c;jre/lib/ext目录下&#xff0c;比如javax…

基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析+爬虫+机器学习)

这里写目录标题 基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析爬虫机器学习)一、项目概述二、微博热词统计析三、微博文章分析四、微博评论分析五、微博舆情分析六、项目展示七、结语 基于Python微博舆情数据爬虫可视化分析系统(NLP情感分析爬虫机器学习) 一、项目概…

Paper Digest|基于在线聚类的自监督自蒸馏序列推荐模型

论文标题&#xff1a; Leave No One Behind: Online Self-Supervised Self-Distillation for Sequential Recommendation 作者姓名&#xff1a; 韦绍玮、吴郑伟、李欣、吴沁桐、张志强、周俊、顾立宏、顾进杰 组织单位&#xff1a; 蚂蚁集团 录用会议&#xff1a; WWW 2024 …

python中pow()函数的使用

在Python中&#xff0c;pow() 函数用于计算指定数字的幂。它的语法如下&#xff1a; pow(x, y) 这个函数返回 x 的 y 次方。相当于 x**y。 pow() 函数也可以接受一个可选的第三个参数&#xff0c;用于指定一个取模值&#xff0c;即计算结果与该模值的余数。其语法如下&#…

Unity编辑器功能 将选中的文件夹复制一份到其他文件夹

[MenuItem("Ab包工具/将选中的文件移动到StreamingAssets文件夹下")] public static void MoveFireToStreamA() { //得到选中文件的数组 Object[] selectobj Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets); i…

U盘文件突然消失:原因分析与恢复策略

U盘遭遇“幽灵”之手&#xff0c;文件不翼而飞 你是否曾遭遇过这样的诡异情况&#xff1a;前一天还好好存放在U盘里的文件&#xff0c;第二天却突然消失得无影无踪&#xff1f;这简直就像是一场无声的灾难&#xff0c;令人措手不及。U盘作为我们日常工作和生活中不可或缺的数据…

I2C系列(三):软件模拟I2C读写24C02

一.目标 PC 端的串口调试软件通过 RS-485 与单片机通信&#xff0c;控制单片机利用软件模拟 I2C 总线对 EEPROM&#xff08;24C02&#xff09; 进行任意读写。 二.硬件简述 2.1 24C02硬件参数 24C02器件地址为0x50&#xff0c;存储容量为256字节&#xff0c;存储单元地址位数…

Docker安装xxl-job并整合到SpringBoot项目

1. 创建数据库 执行如下SQL语句创建相关表 CREATE database if NOT EXISTS xxl_job default character set utf8mb4 collate utf8mb4_general_ci; use xxl_job;SET NAMES utf8mb4; CREATE TABLE xxl_job_info (id int(11) NOT NULL AUTO_INCREMENT,job_group int(11) NOT NUL…

YOLOV8逐步分解(2)_DetectionTrainer类初始化过程

接上篇文章yolov8逐步分解(1)--默认参数&超参配置文件加载继续讲解。 1. 默认配置文件加载完成后&#xff0c;创建对象trainer时&#xff0c;需要从默认配置中获取类DetectionTrainer初始化所需的参数args&#xff0c;如下所示 def train(cfgDEFAULT_CFG, use_pythonFalse…

Java基础语法(二)

前言 Hello&#xff0c;大家好&#xff01;很开心与你们在这里相遇&#xff0c;我是一个喜欢文字、喜欢有趣的灵魂、喜欢探索一切有趣事物的女孩&#xff0c;想与你们共同学习、探索关于IT的相关知识&#xff0c;希望我们可以一路陪伴~ 1. 类型转换 1.1 自动类型转换 什么是自…

政安晨:专栏目录【TensorFlow与Keras机器学习实战】

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 本篇是作者政安晨的专栏《TensorFlow与Keras机器…

AI新工具 小模型也有大智慧Qwen1.5-MoE;大模型动态排行榜;马斯克更新Grok-1.5

✨ 1: Qwen1.5-MoE 阿里巴巴一款小型 MoE 模型&#xff0c;只有 27 亿个激活参数&#xff0c;但性能与最先进的 7B 模型&#xff08;如 Mistral 7B 和 Qwen1.5-7B&#xff09;相匹配。 Qwen1.5-MoE是一个使用混合专家模型&#xff08;Mixture-of-Experts&#xff0c;MoE&…

H5实现3D旋转照片墙教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

从关键词到上下文:GPT 如何重新定义 SEO 策略

如何利用GPT技术革新SEO内容创建&#xff1f; 新的 SEO 格局 探索 SEO 的快速变化&#xff0c;重点关注从以关键字为中心的策略到更深入地了解用户意图和上下文的转变。 GPT 简介及其对内容创建、用户参与和搜索引擎优化 (SEO) 的革命性影响。 了解 GPT&#xff1a;技术范式转…

Stable Diffusion 模型下载:epiCPhotoGasm(真实、照片)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里&#xff0c;订阅后可阅读专栏内所有文章。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 该模型对照片是什么有很高的了解&#xff0c;所以…

Stable Diffusion之核心基础知识和网络结构解析

Stable Diffusion核心基础知识和网络结构解析 一. Stable Diffusion核心基础知识1.1 Stable Diffusion模型工作流程1. 文生图(txt2img)2. 图生图3. 图像优化模块 1.2 Stable Diffusion模型核心基础原理1. 扩散模型的基本原理2. 前向扩散过程详解3. 反向扩散过程详解4. 引入Late…