ArrayList的初始化和扩容源代码解读

13 阅读6分钟

在Java 开发中,ArrayList无疑是我们日常使用最频繁的集合之一。它基于动态数组实现,支持随机访问,并且能够自动调整大小。

但是,它底层到底是如何实现“动态”的呢?当我们新的一个ArrayList时,内存里发生了什么?当我们不断往里面添加元素时,它又是如何进行扩容的?今天,我们就深入JDK 8 的源码,来彻底剥开ArrayList的外衣。

在看核心方法之前,我们需要先了解ArrayList中的几个关键成员变量:

Java

// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;

// 空数组实例,用于明确指定初始容量为 0 时的初始化
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认空数组实例,用于无参构造函数的延迟初始化
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 真正存储数据的数组,transient 表示该字段不被默认序列化
transient Object[] elementData; 

// 实际包含的元素个数
private int size;

1. 构造方法的源代码解读

ArrayList 提供了三个构造方法,它们决定了底层数组 elementData 的初始状态。

1.1 无参构造方法

这是我们最常用的构造方法。

Java

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

解读:

当我们调用 new ArrayList() 时,它并没有立即分配容量为 10 的数组,而是将 elementData 指向了一个静态的空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。这是一种**延迟初始化(懒加载)**的思想,目的是为了节省内存。只有当真正添加第一个元素时,才会将数组扩容到 10。

1.2 指定初始容量的构造方法

如果我们在创建时预知了大概的数据量,通常会使用这个构造方法。

Java

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 创建指定大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 如果指定为 0,则使用静态空数组 EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}

解读:

  • 传入正数:直接分配对应大小的目的数组。
  • 传入0:将日期元素方向空元素数据
  • 注意:这里传入0 赋值的空数组,和无参构造方法赋值的空数组不是同一个。无参构造用的是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这在后续的首次扩容逻辑中会有不同处理。

1.3 包含指定集合的​​构造方法

Java

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray 可能不会返回 Object[] (由于 Java 的向下兼容和某些 bug)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 集合为空,替换为空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

解读:

将传入的集合转为数组赋给 elementData。这里有一个著名的 JDK Bug 修复痕迹(c.toArray 返回的可能不是 Object[] 类型),如果类型不匹配,会通过 Arrays.copyOf 重新拷贝成 Object 数组。


2. 添加()方法的源代码解读

当我们向集合尾部添加元素时,调用的是 add(E e) 方法。它的核心逻辑是:先确保容量足够,然后再放入元素。

Java

public boolean add(E e) {
    // 1. 确保内部容量足够,传入的是所需的最小容量 (当前 size + 1)
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 2. 将元素放入数组尾部,并将 size 加 1
    elementData[size++] = e;
    return true;
}

要想知道扩容的触发时机,我们需要跟进 ensureCapacityInternal 方法:

Java

private void ensureCapacityInternal(int minCapacity) {
    // 首先计算所需的最小容量
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算容量:这就是无参构造函数延迟初始化的真面目
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果当前数组是无参构造时的空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 返回 默认容量 10 和 minCapacity (通常是 1) 中的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 确保具有明确的容量
private void ensureExplicitCapacity(int minCapacity) {
    // 修改次数加 1 (用于 fail-fast 机制)
    modCount++;

    // overflow-conscious code
    // 如果所需的最小容量 大于 当前数组的长度,触发扩容!
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

解读:

  1. 首次添加逻辑: 如果是通过 new ArrayList() 创建的实例,第一次调用 add() 时,calculateCapacity 发现数组是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就会返回 10。随后 10 > 0 成立,直接触发 grow(10),完成从 0 到 10 的跨越。
  2. 常规添加逻辑: 如果 size + 1 大于了当前数组 elementData.length,就会调用 grow() 方法进行扩容。

3. 生长()方法的源代码解读

生长方法是ArrayList动态扩容的核心灵魂所在。

Java

// 数组分配的最大容量 (减去 8 是为了留出一些空间给对象头,避免 OutOfMemoryError)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    
    // 核心扩容算法:新容量 = 旧容量 + (旧容量 / 2) -> 即扩容 1.5 倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 1. 如果扩容 1.5 倍后,依然不够 (例如 addAll 添加多个元素,或者首次扩容时 0 扩容 1.5 倍还是 0)
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity; // 直接把所需最小容量作为新容量
        
    // 2. 如果新容量超过了允许的最大数组大小
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 3. 真正执行扩容:创建新数组,并将旧数组的元素拷贝过去
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 如果 minCapacity 超过了 MAX_ARRAY_SIZE,则分配 Integer.MAX_VALUE,否则分配 MAX_ARRAY_SIZE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

解读:

  1. 1.5 倍扩容: oldCapacity + (oldCapacity >> 1)是最经典的一行代码。位运算>> 1相当于除以2(且效率更高)。这意味着每次正常扩容,容量都会变成原来的1.5倍(比如10 -> 15 -> 22)。

  2. 特殊情况兜底: * 首次扩容旧容量是0,新容量计算出来也是0。此时新容量 < 最小容量(也就是10) 成立,所以新容量被赋值为10。

    • 批量添加:比如调用addAll()一下子添加了100 个元素,扩容1.5 倍不够,就会直接扩容到所需的最小容量大小。
  3. 数组拷贝: 扩容并不是在原内存地址上把空间撑大,而是通过数组.copyOf() 在内存中开辟一块全新的连续空间,然后将原数据复制过去。这属于耗时操作,因此在实际开发中,尽量在new ArrayList 的时候指定初始容量,可以有效避免频繁扩容带来的性能损耗。


总结

  1. ArrayList的无参构造使用的是懒加载,初始数组大小为0,第一次添加时才会扩容为10。
  2. 扩容条件:当所需的最小容量(通常是尺寸 + 1)大于当前数组长度时。
  3. 扩容算法:默认扩容为原来的1.5倍(使用位移运算>> 1)。
  4. 扩容本质:申请一个更大的新数组,然后使用数组.copyOf将旧数据拷贝到新数组中。