ArrayList 的自动扩容是 Java 集合里被问得最多、也最容易答错的一段源码。触发点在哪、新容量怎么算、代价多大,直接决定你会不会在批量写入时踩坑。
下面从 add 入口一路拆到 grow(),把容量推演、构造差异和调优手段讲清楚。
扩容链路总览
一次 add 的完整路径是这样的:
调用 add 方法
↓
size + 1 > elementData.length?
├─ 否 → 直接添加元素
└─ 是 → 触发 grow 扩容
↓
旧容量 > 0?
├─ 否 → 新容量 = DEFAULT_CAPACITY (10)
└─ 是 → 新容量 = 旧容量 + 旧容量 >> 1 (即1.5倍)
↓
新容量仍小于所需最小容量?
├─ 否 → 保持新容量
└─ 是 → 新容量 = 所需最小容量
↓
Arrays.copyOf 复制数组
↓
新容量 > MAX_ARRAY_SIZE?
├─ 否 → 使用新容量
└─ 是 → hugeCapacity 处理 (上限 Integer.MAX_VALUE)
记住三个关键节点:add 判断容量、ensureCapacityInternal 层层传递、grow 真正复制数组。
入口与容量确保
add(E e) 是扩容的唯一常见入口:
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
它把 size+1 作为最小需求容量往下传,最终落到 ensureExplicitCapacity:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
这里有个坑:minCapacity 是需求值,不是最终容量。真正算多大由 grow 决定。
grow 的容量计算
核心只有几行:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
newCapacity = oldCapacity + (oldCapacity >> 1),右移一位等价于除以 2,所以新容量就是旧容量的 1.5 倍。
容量上限的处理单独拎出来:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
超过 MAX_ARRAY_SIZE 会走 hugeCapacity,逼近 Integer.MAX_VALUE 时可能直接 OutOfMemoryError。
1.5 倍是怎么滚起来的
计算方式很直白:
int newCapacity = oldCapacity + (oldCapacity >> 1);
逐次推演一遍,数字比描述更有说服力:
| 扩容次数 | 当前容量 | 计算过程 | 新容量 |
|---|---|---|---|
| 初始化 | 0(空数组) | 首次 add → max(10,1) | 10 |
| 第1次 | 10 | 10 + 10>>1 | 15 |
| 第2次 | 15 | 15 + 7 | 22 |
| 第3次 | 22 | 22 + 11 | 33 |
| 第4次 | 33 | 33 + 16 | 49 |
| 第5次 | 49 | 49 + 24 | 73 |
| 第6次 | 73 | 73 + 36 | 109 |
注意 15 变 22 这一步:15>>1 是 7 不是 7.5,整数右移直接截断,所以实际倍率略低于 1.5。
为什么不是 2 倍或 1.2 倍
| 倍数 | 优点 | 缺点 |
|---|---|---|
| 2 倍 | 扩容次数少 | 内存浪费严重(可能浪费 50%) |
| 1.5 倍 | 平衡性能和内存 | 扩容次数略多于 2 倍 |
| 1.2 倍 | 内存更节省 | 扩容频繁,复制开销大 |
1.5 倍是时间和空间的折中。另外 1.5 倍还有个隐含好处:旧数组空间更容易被后续分配复用,不像 2 倍那样每次都要一块全新的连续内存。
三种构造方法的初始容量差异
无参构造:
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
初始是空数组,不是容量 10。容量 10 是首次 add 时才给的,这点面试高频。
指定初始容量:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException();
}
}
对比一下:
| 构造方式 | 初始数组 | 首次 add 后容量 | 适用场景 |
|---|---|---|---|
| new ArrayList() | 空数组 {} | 10 | 元素数量未知 |
| new ArrayList(0) | 空数组 {} | 1(按需) | 确定元素很少 |
| new ArrayList(100) | 容量 100 数组 | 100 | 已知元素数量(推荐) |

扩容代价与预分配
扩容的本质是 Arrays.copyOf,一次 O(n) 的数组复制。默认从 10 开始添加 1000 万元素,大约要经历 34 次扩容,累计复制量相当可观。
预分配容量 1000 万后,扩容次数为 0,省掉的全是复制开销。
最佳实践就是提前给够:
// 不推荐:频繁扩容
List<String> bad = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
bad.add("item" + i);
}
// 推荐:预分配容量
List<String> good = new ArrayList<>(1000000);
for (int i = 0; i < 1000000; i++) {
good.add("item" + i);
}
// 或先创建再显式扩容
List<String> list = new ArrayList<>();
list.ensureCapacity(1000000);
for (int i = 0; i < 1000000; i++) {
list.add("item" + i);
}
如果容量是逐步确定的,用 ensureCapacity 手动补:
public void ensureCapacity(int minCapacity) {
if (minCapacity > elementData.length
&& !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
&& minCapacity <= DEFAULT_CAPACITY)) {
modCount++;
grow(minCapacity);
}
}
旧数组、缩容与批量添加
扩容后旧数组失去引用,交给 GC 回收:
elementData = Arrays.copyOf(elementData, newCapacity);
大量删除后想释放多余内存,用 trimToSize:
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
它把底层数组缩到和 size 一样大,属于主动缩容:
ArrayList<String> list = new ArrayList<>(1000000);
// ... 大量删除操作后
list.trimToSize();
批量添加时,addAll 会先算好总需求一次性扩容,避免多次小批量触发复制:
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew);
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
几个高频疑问
Q1:最大容量是多少?理论上是 Integer.MAX_VALUE,实际实现里是 Integer.MAX_VALUE – 8:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
减 8 是因为部分 JVM 会在数组头存对象信息,超限可能抛 OutOfMemoryError。
Q2:size() 和数组长度为什么不同?elementData.length 是物理容量,size 是逻辑元素个数,恒有 size <= elementData.length。
Q3:扩容为什么改 modCount?虽然 size 没变,但底层数组结构变了,属于结构性修改,用于 fail-fast:
int expectedModCount = modCount;
Q4:和 Vector 的区别?ArrayList 扩容 1.5 倍、非线程安全;Vector 扩容 2 倍(可指定增量)、方法带 synchronized,默认容量同为 10。
核心要点
| 要点 | 内容 |
|---|---|
| 触发条件 | size + 1 > elementData.length |
| 扩容倍数 | 1.5 倍(oldCapacity + (oldCapacity >> 1)) |
| 首次扩容 | 空数组 → 容量 10 |
| 核心方法 | grow() → Arrays.copyOf() |
| 时间复杂度 | 扩容 O(n),均摊 O(1) |
| 最佳实践 | 预分配容量(已知大小时) |
| 最大容量 | Integer.MAX_VALUE – 8 |
| 缩容方法 | trimToSize() |
一句话记忆:扩容 1.5 倍,首次添加变 10;预分配省复制,性能优化记心中。
代码口诀:
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>(10000);
list2.ensureCapacity(20000);