1. 深入理解ArrayList源码

1. ArrayList简介

ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。

ArrayList 继承了 AbstractList ,并实现了 List 接口。

主要特点

  1. 动态扩容:ArrayList的容量可以自动增长,不需要预先指定固定大小

  2. 随机访问:由于基于数组实现,可以通过索引快速访问元素(时间复杂度O(1))

  3. 非同步:ArrayList不是线程安全的

  4. 允许null元素:可以存储null值

  5. 有序集合:保持元素的插入顺序

基础用法:

复制代码
// 创建ArrayList
ArrayList<String> list = new ArrayList<>();

// 添加元素
list.add("Java");
list.add("Python");
list.add("C++");

// 访问元素
String first = list.get(0);  // 获取第一个元素

// 修改元素
list.set(1, "JavaScript");  // 修改第二个元素

// 删除元素
list.remove(2);  // 删除第三个元素

// 遍历
for (String language : list) {
    System.out.println(language);
}

与数组的比较

  1. 大小灵活性:数组大小固定,ArrayList大小可变

  2. 功能丰富性:ArrayList提供了更多内置方法(add, remove等)

  3. 性能:数组在内存使用和访问速度上略优


2. ArrayList源码分析

下面的一切都是基于jdk1.8 进行分析

2.1 ArrayList的成员变量和构造函数

了解成员变量和构造函数是分析源码的第一步

成员变量:

java 复制代码
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {

        // 这些都是成员变量


        /**
         * 作用:序列化版本控制标识符
         * 说明:用于确保序列化和反序列化过程中类的版本兼容性
         */
        private static final long serialVersionUID = 8683452581122892189L;

        /**
         * 作用:默认初始容量
         * 说明:当使用无参构造函数创建ArrayList且第一次添加元素时,默认创建的数组大小
         */
        private static final int DEFAULT_CAPACITY = 10;

        /**
         * 作用:空数组共享实例
         * 说明:用于构造具有指定初始容量为0的ArrayList时使用
         */
        private static final Object[] EMPTY_ELEMENTDATA = new Object[0];

        /**
         * 作用:默认容量空数组共享实例
         * 说明:用于无参构造函数创建的 ArrayList,与 EMPTY_ELEMENTDATA 区分是为了知道第一次添加元素时需要扩容多少
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];

        /**
         * 作用:实际存储元素的数组缓冲区
         * 说明:
         *      transient修饰表示序列化时不会自动序列化此字段
         *      ArrayList通过自定义的 writeObject 和 readObject 方法来控制序列化过程
         *      实际类型是 Object[],但通过泛型在运行时进行类型检查
         */
        transient Object[] elementData;

        /**
         * 作用:ArrayList中实际元素的数量
         * 说明:
         *      不等于elementData数组的长度(容量)
         *      表示列表中实际包含的元素个数
         *      size <= elementData.length
         */
        private int size;

        /**
         * 作用:数组最大容量限制
         * 说明:
         *      值为2147483639 (Integer.MAX_VALUE - 8)
         *      减 8 是因为某些虚拟机在数组中保留 header words
         *      尝试分配更大的数组可能导致 OutOfMemoryError
         */
        private static final int MAX_ARRAY_SIZE = 2147483639;
}

构造函数:

java 复制代码
        // 带初始容量的构造参数
        public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else {
                if (initialCapacity != 0) {
                    throw new IllegalArgumentException("Illegal Capacity: " +                                             initialCapacity);
                }

                this.elementData = EMPTY_ELEMENTDATA;
            }

        }

        // 无参构造函数
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

        // Collection 是集合的父接口
        // 将collection对象转换成数组,然后将数组的地址的赋给elementData
        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 {
                elementData = EMPTY_ELEMENTDATA;
            }
        }

2.2 ArrayList的添加扩容(第一次添加)

基本扩容流程

  1. 初始容量:默认初始容量为10(在第一次添加元素时真正分配)

  2. 扩容触发条件:当尝试添加元素时(add操作),发现当前元素数量等于数组容量

  3. 扩容大小:新容量 = 旧容量 + (旧容量 >> 1)(即大约1.5倍)

关键源码分析

java 复制代码
// 添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

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

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需最小容量大于当前数组长度,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

// 核心扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量=旧容量*1.5
    if (newCapacity - minCapacity < 0) // 特殊情况处理
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 处理超大容量
        newCapacity = hugeCapacity(minCapacity);
    // 复制数据到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

接下来我们一步一步分析,先看代码:

先创建 ArrayList 并添加数据:

java 复制代码
ArrayList<Integer> list = new ArrayList<>(); // 初始空数组
list.add(1);  // 第一次添加,扩容到10

初始化空数组时会调用无参构造,创建一个空的 ArrayList

java 复制代码
// 无参构造函数
public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

当我们使用 add 方法添加数据时,方法内部就是执行添加、扩容操作。

具体流程如下:

  • add 方法(添加元素):
java 复制代码
// 添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

确保内部容量足够所以调用 ensureCapacityInternal 方法验证,

size 是 实际元素数量。

  • ensureCapacityInternal 方法(确保内部容量):
java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

在这个方法内部还有一个方法 calculateCapacity(elementData, minCapacity),

calculateCapacity 方法是用来计算容量

参数:

elementData :实际存储元素的数组缓冲区。

这个 elementData 其实就是成员变量当中的 transient Object[] elementData;

它是通过无参构造附的值 实际上 elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA

  • calculateCapacity 方法(计算容量):
java 复制代码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
     return elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA ? Math.max(DEFAULT_CAPACITY, minCapacity) : minCapacity;
}

// 不同版本可能略有差异,有的是用 if 语句判断
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA){
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA :默认容量空数组共享实例 。

DEFAULT_CAPACITY :默认初始容量。
通过**calculateCapacity 方法(计算容量)**拿到返回值,

由于是第一次进行扩容,其实拿到的返回值就是 10。

然后回到 **ensureCapacityInternal 方法(确保内部容量),**此时的方法应为:

java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(10);
}

然后继续调用 ensureExplicitCapacity 方法()。

  • ensureExplicitCapacity 方法(确保显式容量):
java 复制代码
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需最小容量大于当前数组长度,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

这个方法里面设计一个判断,如果所需最小容量大于当前数组长度(相减大于 0),则扩容

扩容方法就是 grow 方法。

  • grow 方法(扩容):
java 复制代码
// 核心扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量=旧容量*1.5
    if (newCapacity - minCapacity < 0) // 特殊情况处理
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 处理超大容量
        newCapacity = hugeCapacity(minCapacity);
    // 复制数据到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

这个方法中,主要进行以下操作:

  • 第一行代码先去拿到了数组的容量,然后赋值给 oldCapacity(旧容量)。
  • 第二行就是扩容操作,>>1 这是位运算,向右移动 1 位,也就是 ++除以 2++ ,再加上原来的长度(容量),所以 新容量=旧容量*1.5 。
  • 最后一行也就是数组拷贝,复制数据到新数组,新数组的长度也就是 newCapacity(新容量)。
  • 新数组与原数组地址不同,但数组内储存的数据是相同的

数组拷贝完成之后,就会回到最开始的 **add 方法(添加元素),**继续执行

继续执行add 方法(添加元素):

java 复制代码
// 添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

执行 第二行 elementData[size++] = e;

为 数组的 size 下标处进行赋值,值为 e,也就是咱们最开始传进来的 "1"。

java 复制代码
ArrayList<Integer> list = new ArrayList<>(); // 初始空数组
list.add("1");  // 第一次添加,扩容到10

赋完值后,执行 size++,

注意:

// 复制数据到新数组
elementData = Arrays.copyOf(elementData, newCapacity);

  1. Arrays.copyOf() 会创建新数组 ,所以 elementData 不再指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA

  2. 元素是引用复制 (新数组和旧数组的元素指向相同的对象),但 数组对象本身是新创建的(地址不同)。

  3. 扩容不会影响原有元素的对象,只是重新分配了一个更大的数组并复制引用。

以上就是第一次扩容的全过程。


2.3 ArrayList的添加扩容(第2 - 10 次添加)

第 2------10 次添加,是不会走扩容操作的,因为在第一次扩容后,数组长度就是 10,容量一直是足够。

我们还是继续分析一波:

我们继续进行第2------10次添加:

java 复制代码
ArrayList<Integer> list = new ArrayList<>(); // 初始空数组
list.add(1);  // 第一次添加,扩容到10
for(int i = 2; i < 10; i++) {
    list.add(i);
}

第2------9次肯定是没有问题的,咱们主要说第 10 次添加。

第 10 次添加:

  • add 方法:
java 复制代码
// 添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}

当进行第 10 次添加,此时 size 的值应该为 9,因为添加了九次。

size + 1 = 10 ,所以 10 继续进入方法 ensureCapacityInternal执行。

  • ensureCapacityInternal 方法,及其内部方法:
java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需最小容量大于当前数组长度,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

进入 ensureCapacityInternal 方法 中,继续调用 calculateCapacity 方法。

此时传入 calculateCapacity 的参数中 minCapacity = 10 。

calculateCapacity 中:

由于经过了第一次扩容,条件elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA不在成立,

所以直接返回minCapacity,也是 10。

然后继续 ensureExplicitCapacity(10) :

  • minCapacity = 10
  • elementData.length = 10(当前数组长度)
  • minCapacity - elementData.length > 0,条件不成立,所以不会进行扩容。

所以,第2 - 10 次添加都不会进行扩容。


2.4 ArrayList的添加扩容(第 11 次添加)

第 11 次添加,这个超出数组长度肯定要进行扩容。

我们还是一步一步分析~~~

我们继续进行第 11 次添加:

java 复制代码
ArrayList<Integer> list = new ArrayList<>(); // 初始空数组
list.add(1);  // 第一次添加,扩容到10
for(int i = 2; i < 10; i++) {
    list.add(i);
}
list.add(11); 

第 11 次添加:

  • add 方法:
java 复制代码
// 添加元素方法
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // 确保容量足够
    elementData[size++] = e;
    return true;
}
  • 此时,size = 10 ,size + 1 = 11
  • 11 代入 ensureCapacityInternal 方法。
  • ensureCapacityInternal 方法,及其内部方法:
java 复制代码
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

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

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // 如果所需最小容量大于当前数组长度,则扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

先执行 calculateCapacity方法:

  • minCapacity = 11
  • 由于经过了数组拷贝,条件elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA不在成立
  • 直接返回 minCapacity

再执行 ensureExplicitCapacity方法:

  • minCapacity = 11
  • elementData.length = 10
  • 所以 minCapacity - elementData.length > 0 ,条件成立
  • 执行 grow 方法进行扩容
  • grow 方法(扩容):
java 复制代码
// 核心扩容方法
private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量=旧容量*1.5
    if (newCapacity - minCapacity < 0) // 特殊情况处理
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0) // 处理超大容量
        newCapacity = hugeCapacity(minCapacity);
    // 复制数据到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • minCapacity = 11
  • 第一行 oldCapacity = 10 (原来数组长度)
  • 第二行 newCapacity = 10 + 5 = 15 (新数组长度)
  • 进行 Arrays.copyOf 拷贝,新数组和原来数组的地址是不一致的。

拷贝完成之后,继续回到 add 方法为数组的第 11 号位进行赋值。

这就是后续扩容的步骤。


3. 问题总结

3.1 ArrayList的底层原理实现?

  • ArrayList底层是用动态的数组实现的
  • ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
  • ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
  • ArrayList在添加数据的时候进行的步骤:
    1. 确保数组已使用长度(size)加1之后足够存下下一个数据
    2. 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    3. 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。
    4. 返回添加成功布尔值。

3.2 如果在创建ArrayList时声明数组长度,list集合会扩容几次?

eg:ArrayList list=new ArrayList(10)
指定初始大小为 10,会进行下面的构造函数进行创建:

java 复制代码
 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);
    }
}

关键逻辑解析:

  1. initialCapacity > 0

    • 直接创建一个大小为 initialCapacityObject[] 数组,赋值给 elementData

    • 示例new ArrayList(10)elementData = new Object[10](但此时 size=0,因为还没添加元素)。

  2. initialCapacity == 0

    • 使用 EMPTY_ELEMENTDATA(共享的空数组),避免不必要的内存分配。

    • 示例new ArrayList(0)elementData = EMPTY_ELEMENTDATA

  3. initialCapacity < 0

    • 抛出 IllegalArgumentException 异常。

    • 示例new ArrayList(-1) → 直接报错。


很显然"直接创建一个大小为 initialCapacityObject[] 数组,赋值给 elementData",

所以并不会进行扩容,因此答案是 0 次。


相关推荐
Code季风3 分钟前
测试驱动开发(TDD)实战:在 Spring 框架实现中践行 “红 - 绿 - 重构“ 循环
java·驱动开发·后端·spring·设计模式·springboot·tdd
hello早上好11 分钟前
JPA、缓存、数据源与连接池、简介
java·mybatis
Kiri霧19 分钟前
细谈kotlin中缀表达式
开发语言·微信·kotlin
想要成为祖国的花朵24 分钟前
Java_Springboot技术框架讲解部分(二)
java·开发语言·spring boot·spring
Q_Q51100828530 分钟前
python的小学课外综合管理系统
开发语言·spring boot·python·django·flask·node.js
勤奋的知更鸟41 分钟前
JavaScript 性能优化实战:深入性能瓶颈,精炼优化技巧与最佳实践
开发语言·javascript·性能优化
vvilkim43 分钟前
深入理解设计模式:原型模式(Prototype Pattern)
java·设计模式·原型模式
通域1 小时前
Mac (m1) Java 加载本地C共享库函数 .dylib 函数 Unable to load library ‘liblicense‘
java·python·macos
hqxstudying1 小时前
Java行为型模式---模板方法模式
java·开发语言·设计模式·代码规范·模板方法模式
weixin_443290691 小时前
【脚本系列】如何使用 Python 脚本对同一文件夹中表头相同的 Excel 文件进行合并
开发语言·python·excel