在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);
}
解读:
- 首次添加逻辑: 如果是通过
new ArrayList()创建的实例,第一次调用add()时,calculateCapacity发现数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就会返回10。随后10 > 0成立,直接触发grow(10),完成从 0 到 10 的跨越。 - 常规添加逻辑: 如果
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.5 倍扩容:
oldCapacity + (oldCapacity >> 1)是最经典的一行代码。位运算>> 1相当于除以2(且效率更高)。这意味着每次正常扩容,容量都会变成原来的1.5倍(比如10 -> 15 -> 22)。 -
特殊情况兜底: * 首次扩容:
旧容量是0,新容量计算出来也是0。此时新容量 < 最小容量(也就是10) 成立,所以新容量被赋值为10。- 批量添加:比如调用
addAll()一下子添加了100 个元素,扩容1.5 倍不够,就会直接扩容到所需的最小容量大小。
- 批量添加:比如调用
-
数组拷贝: 扩容并不是在原内存地址上把空间撑大,而是通过
数组.copyOf()在内存中开辟一块全新的连续空间,然后将原数据复制过去。这属于耗时操作,因此在实际开发中,尽量在new ArrayList 的时候指定初始容量,可以有效避免频繁扩容带来的性能损耗。
总结
ArrayList的无参构造使用的是懒加载,初始数组大小为0,第一次添加时才会扩容为10。- 扩容条件:当所需的最小容量(通常是
尺寸 + 1)大于当前数组长度时。 - 扩容算法:默认扩容为原来的1.5倍(使用位移运算
>> 1)。 - 扩容本质:申请一个更大的新数组,然后使用
数组.copyOf将旧数据拷贝到新数组中。