ArrayList 的扩容逻辑看起来简单,真正踩过坑的人才知道细节不少。下面从触发条件一路拆到容量上限,再聊几个生产环境里容易翻车的地方。
扩容到底什么时候发生
判断条件只有一句:size + 1 > elementData.length。
add 方法先调 ensureCapacityInternal 确认容量,再把元素塞到数组末尾。这是所有添加操作的统一入口。
调用 add 方法
↓
size + 1 > elementData.length?
↓ 是
触发 grow 扩容
↓
计算新容量
↓
旧容量 > 0?
├─ 是 → 新容量 = 旧容量 + 旧容量 >> 1 (即1.5倍)
└─ 否 → 新容量 = DEFAULT_CAPACITY (10)
↓
新容量仍小于所需最小容量?
├─ 是 → 新容量 = 所需最小容量
└─ 否 → 保持计算值
↓
Arrays.copyOf 复制数组
↓
新容量 > MAX_ARRAY_SIZE?
├─ 是 → 调用 hugeCapacity 处理,上限为 Integer.MAX_VALUE
└─ 否 → 直接使用新容量
容量确保链分两步走:先处理默认空数组(首次添加时),再判断是否需要真正扩容。
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e; // 添加元素
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 修改计数器,用于检测并发修改
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 容量不足时触发扩容
}
modCount 这个字段容易被忽略。它专门用于检测并发修改,迭代器遍历时会比对它的值,一旦发现变化就抛 ConcurrentModificationException。
grow 方法内部做了什么
真正的扩容动作集中在 grow。
新容量公式:oldCapacity + (oldCapacity >> 1),也就是 1.5 倍。
如果算出来的新容量还是不够 minCapacity,就直接采用 minCapacity。最后通过 Arrays.copyOf 建新数组并搬数据。
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); // 复制数组
}
容量逼近上限时还有一层保护:超过 MAX_ARRAY_SIZE(通常是 Integer.MAX_VALUE – 8)后,按实际需要返回 Integer.MAX_VALUE 或直接抛 OOM。
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 溢出检查
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
1.5 倍是怎么算出来的
是右移,等价于除以 2 向下取整。所以增长序列是:10 → 15 → 22 → 33 → 49。
用几个节点感受一下:
| 添加次数 | 当前容量 | 触发扩容时容量 | 新容量 |
|---|---|---|---|
| 1-10 | 10 | 11 | 15 |
| 11-15 | 15 | 16 | 22 |
| 16-22 | 22 | 23 | 33 |
| 23-33 | 33 | 34 | 49 |
为什么不是 2 倍?1.5 倍是空间和时间之间的折中。倍增浪费空间更明显,1.5 倍省一些;扩容次数虽然更多,但两者均摊时间复杂度都是 O(1)。
构造方法决定初始容量
无参构造初始容量是 0,第一次 add 才扩到 10。指定容量则直接建对应大小的数组,后续不用再折腾。
添加元素 → 检查容量 → 容量足够?
├─ 是 → 直接添加
└─ 否 → 计算新容量 → 复制数组 → 添加元素
扩容前:[e1, e2, e3, ..., e10, null, null, null] (容量10)
扩容后:[e1, e2, e3, ..., e10, null, null, ..., null] (容量15)
| 构造方法 | 初始容量 | 首次扩容目标 | 适用场景 |
|---|---|---|---|
| 无参构造 | 0→10 | 10 | 元素数量不确定 |
| 指定容量 | 指定值 | 指定值 | 数量基本确定 |
| 集合构造 | 集合大小 | 集合大小 | 从已有集合创建 |
扩容的代价与容量预分配
每次扩容都走 Arrays.copyOf,这是 O(n) 操作。分配新数组、复制旧元素、旧数组等待 GC,三步一个都跑不掉。
频繁扩容的性能损失很直观。能预估数量时,直接指定初始容量最省事。
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
批量添加前也可以调 ensureCapacity 一次性扩到位,避免多次扩容。
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);
}
这里补一个实际部署中容易遇到的报错:如果构造 ArrayList 时传入负数,比如 new ArrayList(-1),会直接抛 IllegalArgumentException: Illegal Capacity: -1。根因是构造方法里对 capacity 做了小于 0 的判断。解决方式就是保证传入值非负,或者干脆用无参构造。
旧数组、缩容和批量操作
Arrays.copyOf 执行后,旧数组不再被引用,交给 JVM 的 GC 回收,不用手动处理。
trimToSize 能把容量压到实际元素数量,释放多余内存,但频繁调用会有性能开销,不建议在生产环境当常规操作。
// 不推荐:频繁扩容
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add("item" + i); // 多次扩容
}
// 推荐:预分配容量
ArrayList<String> list = new ArrayList<>(10000);
for (int i = 0; i < 10000; i++) {
list.add("item" + i); // 无扩容
}
添加大量元素时,几个实用原则:预估容量用指定构造;批量添加用 addAll 而不是循环 add;后续还会继续添加就别急着 trimToSize。
几个高频疑问
最大容量是多少?理论上 Integer.MAX_VALUE(约 21 亿),实际受 MAX_ARRAY_SIZE 限制,通常是 Integer.MAX_VALUE – 8,因为部分 VM 会保留头部信息。
size() 和数组长度为什么不一样?size() 是实际元素数量,elementData.length 是容量。扩容机制决定了容量总是大于等于元素数量。
modCount 为什么增加?每次结构修改(添加、删除、扩容)都会加。迭代器靠它检测并发修改。
ArrayList 和 Vector 扩容区别?ArrayList 是 1.5 倍、非线程安全;Vector 默认 2 倍、方法带 synchronized。Vector 已经过时,需要线程安全时用 Collections.synchronizedList 或 CopyOnWriteArrayList 更合适。
验证扩容行为的实操方法
想确认扩容是否按预期发生,可以开反射读 elementData.length。
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
? 0
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
判定标准很直接:添加第 11 个元素后容量应为 15,第 16 个元素后应为 22。如果对不上,检查是不是构造时已经指定了初始容量。
一句话收尾
ArrayList 扩容 = 1.5 倍增长 + 数组复制 + 均摊 O(1)。预估容量是性能优化的关键,别等它一次次搬家。
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
初始状态:size=0, capacity=10 (首次添加时扩容)
添加10个元素:size=10, capacity=10
添加第11个元素:触发扩容,capacity=15
添加15个元素:size=15, capacity=15
添加第16个元素:触发扩容,capacity=22
...以此类推