ArrayList 是 Java 集合框架中最常用的实现类之一,作为基于动态数组的列表结构,其核心优势在于支持动态扩容 —— 这也是它区别于普通数组(固定长度)的关键特性。本文将从底层原理、源码分析、扩容流程等维度,全方位拆解 ArrayList 的扩容机制,帮你彻底理解其背后的设计逻辑。

一、ArrayList 扩容的核心背景

普通数组在创建时必须指定长度,且一旦创建无法修改;而 ArrayList 通过动态扩容解决了这一痛点:当向 ArrayList 中添加元素时,若底层数组已满,会自动创建一个更大的新数组,并将原数组的元素复制到新数组中,从而实现 “动态增长”。

关键前置知识

在分析扩容前,先明确 ArrayList 的几个核心成员变量(基于 JDK 8):

// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组(用于空构造器初始化)
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组(用于无参构造器,区别于EMPTY_ELEMENTDATA,标记首次添加元素时的扩容)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 底层存储元素的数组(真正存放数据的地方)
transient Object[] elementData;
// ArrayList中实际元素的数量
private int size;
// 数组的最大容量(避免OOM)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

二、ArrayList 扩容的触发条件

扩容的核心触发时机是:当添加元素时,发现当前元素数量(size)即将超过底层数组的长度(elementData.length)

最典型的触发场景是调用add(E e)方法时,源码逻辑如下:

public boolean add(E e) {
    // 1. 检查容量是否足够,不足则扩容
    ensureCapacityInternal(size + 1); 
    // 2. 新增元素,size自增
    elementData[size++] = e;
    return true;
}

核心逻辑在ensureCapacityInternal方法中 —— 它会判断 “添加新元素后所需的最小容量” 是否超过当前数组长度,若超过则触发扩容。

三、扩容的完整流程(源码级拆解)

为了清晰展示扩容逻辑,我们按 “方法调用链” 逐步分析,所有代码均来自 JDK 8 源码(关键部分添加注释)。

步骤 1:计算所需最小容量(ensureCapacityInternal)

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

// 核心:计算“真正需要的最小容量”
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 情况1:如果是无参构造的空ArrayList(首次添加元素)
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 取默认容量(10)和minCapacity的最大值(首次扩容直接到10)
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 情况2:非空数组/指定初始容量的空数组,直接返回minCapacity
    return minCapacity;
}

步骤 2:判断是否需要扩容(ensureExplicitCapacity)

private void ensureExplicitCapacity(int minCapacity) {
    // 快速失败(modCount记录结构修改次数,迭代器会校验)
    modCount++;

    // 核心判断:所需最小容量 > 当前数组长度 → 触发扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

步骤 3:核心扩容逻辑(grow 方法)

grow是 ArrayList 扩容的核心方法,负责创建新数组、复制元素,也是最关键的部分:

private void grow(int minCapacity) {
    // 1. 记录原数组长度
    int oldCapacity = elementData.length;
    // 2. 计算新容量:oldCapacity + (oldCapacity >> 1) → 相当于原容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 3. 处理特殊情况:新容量 < 所需最小容量(比如批量添加大量元素)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 4. 处理超大容量:新容量超过MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);

    // 5. 核心操作:复制原数组到新数组(Arrays.copyOf是native方法,效率高)
    elementData = Arrays.copyOf(elementData, newCapacity);
}

// 处理超大容量的边界情况
private static int hugeCapacity(int minCapacity) {
    // 所需容量超过Integer.MAX_VALUE → 抛出OOM
    if (minCapacity < 0) 
        throw new OutOfMemoryError();
    // 若所需容量超过MAX_ARRAY_SIZE,直接用Integer.MAX_VALUE,否则用MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

四、扩容机制的关键细节

1. 扩容倍数:1.5 倍的设计逻辑

ArrayList 默认扩容为原容量的1.5 倍(通过位运算oldCapacity >> 1实现,比乘法* 1.5更高效),这个设计是 “时间 / 空间” 的平衡:

  • 若扩容倍数太小(比如 1.1 倍):会导致频繁扩容,频繁复制数组,性能损耗大;
  • 若扩容倍数太大(比如 2 倍):会浪费更多内存,空间利用率低;
  • 1.5 倍是 JDK 团队权衡后的最优选择,兼顾性能和内存利用率。

2. 首次扩容的特殊情况

  • 无参构造的 ArrayList:初始时elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组),首次添加元素时直接扩容到 10(而非 1.5 倍);
  • 指定初始容量的构造器(如new ArrayList(5)):初始数组长度为 5,首次满了之后扩容到 7(5+2),再满扩容到 10(7+3),以此类推。

3. 手动扩容:提前优化性能

频繁扩容会因为数组复制带来性能损耗,若提前知道元素数量,可通过ensureCapacity(int minCapacity)手动指定容量,避免多次自动扩容:

// 示例:提前指定容量,避免扩容
ArrayList<String> list = new ArrayList<>();
// 已知要添加1000个元素,手动扩容到1000
list.ensureCapacity(1000);
for (int i = 0; i < 1000; i++) {
    list.add("test" + i);
}

4. 扩容的性能损耗点

扩容的核心开销在于Arrays.copyOf—— 它会创建新数组并复制原数组的所有元素,时间复杂度为 O (n)。因此:

  • 若业务场景需要频繁添加大量元素,建议初始化时指定合适的容量;
  • 若 ArrayList 元素量极大,扩容时可能触发 GC,甚至 OOM(需关注内存使用)。

五、扩容机制的可视化流程

六、常见面试题 & 易错点

  1. ArrayList 初始容量是 10 吗?不一定:无参构造的 ArrayList 初始是空数组,首次添加元素才扩容到 10;指定初始容量的构造器,初始容量为指定值。

  2. 扩容时元素是如何复制的?底层调用System.arraycopy(native 方法),属于浅拷贝 —— 若元素是引用类型,新数组和原数组指向同一个对象。

  3. 为什么扩容倍数是 1.5 倍,而不是 2 倍?1.5 倍可减少内存浪费,且通过位运算实现更高效;2 倍虽扩容次数少,但容易导致大量空闲空间无法利用。

  4. ArrayList 最大容量是多少?理论上是Integer.MAX_VALUE(2^31-1),但 JDK 限制了MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8(避免某些虚拟机的内存预留),仅当所需容量超过该值时,才会使用Integer.MAX_VALUE

总结

  1. ArrayList 扩容的核心触发条件是 “添加元素时所需最小容量超过当前数组长度”,核心方法是grow()
  2. 默认扩容倍数为原容量的 1.5 倍(位运算实现),无参构造的 ArrayList 首次扩容直接到 10;
  3. 扩容的本质是创建新数组 + 复制原元素(浅拷贝),频繁扩容会损耗性能,建议提前指定初始容量或手动扩容。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐