ArrayList 源码与扩容机制
候选人小王在字节面试时,被问到:"ArrayList 的默认容量是多少?"
他脱口而出:"16!"
面试官点点头:"第一次 add 之后呢?"
小王愣了一秒:"还是16吧?"
面试官笑而不语。
【面试官心理】
这道题我用来筛选两类候选人:一类是"背过八股"的,知道 ArrayList 默认容量是 16;另一类是"真正动手看过源码"的,知道 ArrayList 的默认容量是 0,第一次 add 才扩容到 10。能说出"为什么是 10 不是 16"的,基本是 P6+ 了。
一、ArrayList 的容量陷阱 🔴
1.1 默认容量到底是多少
很多同学会说 ArrayList 默认容量是 16。错!
ArrayList 的默认构造函数创建的 ArrayList,容量是 0:
// JDK 1.8 ArrayList.java
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA; // EMPTY_ELEMENTDATA = {}
}
是的,你没看错,就是空数组。只有当你第一次调用 add() 时,才会触发 grow() 方法扩容到 10:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确认容量
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
// DEFAULT_CAPACITY = 10
}
ensureExplicitCapacity(minCapacity);
}
⚠️
ArrayList 默认容量是 0,不是 16!只有第一次 add 时才扩容到 10。这个设计是为了节省内存,避免创建空 ArrayList 时就占用 16 个 Object 引用的空间。
1.2 常见翻车现场
我见过太多这样的代码:
// 生产事故代码
List<Order> orders = new ArrayList<>();
for (Order order : orderList) {
orders.add(order); // 每次 add 都要检查容量
}
假设 orderList 有 100 万条数据,这个循环会触发多少次扩容?
- 初始容量 0 → 扩容到 10
- 10 → 扩容到 15(1.5 倍)
- 15 → 扩容到 22
- 22 → 扩容到 33
- ...
计算一下:从 0 到 100 万,需要扩容大约 log(1000000/10) / log(1.5) ≈ 33 次。
每次扩容都要 System.arraycopy() 迁移所有元素,O(n) 的迁移操作执行 33 次,总复杂度直接退化成 O(n²)!
💡
正确做法:预估数据量,预设容量。
List<Order> orders = new ArrayList<>(orderList.size());
二、扩容机制源码解析 🔴
2.1 grow() 方法详解
ArrayList 的扩容核心在 grow() 方法:
private void grow(int minCapacity) {
// 旧容量
int oldCapacity = elementData.length;
// 新容量 = 旧容量 + 旧容量/2 = 1.5 倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果 1.5 倍还不够,直接用 minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 不能超过 MAX_ARRAY_SIZE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
2.2 扩容流程图
graph TD
A[add 元素] --> B{容量够吗?}
B -->|不够| C[计算新容量 = old + old/2]
B -->|够| D[直接插入]
C --> E{新容量 `<` 最小需求?}
E -->|是| F[新容量 = 最小需求]
E -->|否| G{新容量 `>` 最大限制?}
G -->|是| H[申请 Integer.MAX_VALUE]
G -->|否| I[正常容量]
F --> J[Arrays.copyOf 扩容]
I --> J
J --> K[插入元素]
D --> K
2.3 为什么是 1.5 倍而不是 2 倍
面试官追问:"为什么 JDK 选择 1.5 倍扩容,而不是 2 倍?"
这是经典的设计权衡题:
空间角度:2 倍扩容意味着新数组有 50% 的空位;1.5 倍只有 33% 的空位。
时间角度:扩容频率和扩容倍数成反比。1.5 倍扩容频率略高,但迁移成本更小。
JDK 设计者的权衡:1.5 倍是一个经过数学验证的经验值,既不会因为扩容太频繁而影响性能,也不会因为太激进而导致严重内存浪费。
📖 点击展开 1.5 倍扩容的数学推导
考虑 n 次插入的总代价:
总代价 = 扩容次数 × 迁移成本
= Σ (每次迁移的元素数量)
设扩容倍数为 k,初始容量为 c,第 i 次扩容后的容量为 c * k^i。
当插入第 N 个元素时:
- 如果用 k 倍扩容,需要
log_k(N/c) 次扩容
- 每次扩容迁移的元素数量呈指数增长
1.5 被认为是 均摊复杂度最优 的选择。
2.4 System.arraycopy() 元素迁移
JDK 使用 Arrays.copyOf() 进行数组复制:
elementData = Arrays.copyOf(elementData, newCapacity);
// 内部实现
public static <T> T[] copyOf(T[] original, int newLength) {
T[] copy = (T[]) java.lang.reflect.Array.newInstance(
original.getClass().getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
return copy;
}
System.arraycopy() 是 native 方法,直接操作内存复制,效率很高。但再高的效率也扛不住频繁调用。
【学习小结】
ArrayList 扩容机制要点:
- 默认容量是 0,第一次 add 才扩容到 10
- 扩容倍数是 1.5(
old + (old >> 1))
- 扩容通过
System.arraycopy() 迁移元素
- 循环中添加必须预设容量
三、容量设置的艺术 🟡
3.1 构造函数对比
ArrayList 有三种构造函数:
// 1. 无参构造函数 - 容量为 0
ArrayList list1 = new ArrayList();
// 2. 指定初始容量 - 推荐做法
ArrayList list2 = new ArrayList<>(100);
// 3. 传入集合 - 容量等于集合大小
ArrayList list3 = new ArrayList<>(existingCollection);
3.2 预设容量的最佳实践
// 场景 1:已知数据量
List<User> users = new ArrayList<>(expectedUserCount);
// 场景 2:批量添加
List<Order> orders = new ArrayList<>(orderList.size());
orders.addAll(orderList);
// 场景 3:不确定但可能很大
// 使用 Integer.MAX_VALUE 有风险!
List<Data> data = new ArrayList<>(1 << 20); // 约 100 万
// 场景 4:真正需要无限容量
// 用 ArrayList 的容量没有上限
// 但数组本身最大是 Integer.MAX_VALUE
3.3 trimToSize() 的使用
如果 ArrayList 已经使用完毕,不再添加元素,可以调用 trimToSize() 释放多余的容量:
List<String> words = new ArrayList<>(1000);
// ... 添加 100 个元素
words.trimToSize(); // 容量从 1000 降到 100,节省内存
💡
trimToSize() 用于优化内存占用。在内存敏感场景(如 Android 开发、微服务处理大数据)很有用。
四、边界条件与追问 🟡
4.1 最大容量限制
ArrayList 的最大容量不是无限制的:
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;
}
为什么是 Integer.MAX_VALUE - 8?因为一些 JVM 实现会在数组头部预留 8 字节的 header 空间。
4.2 扩容阈值计算
面试官追问:"给定初始容量 0,add() 10 次会扩容几次?"
add(1): 0 → 10 (第 1 次扩容)
add(2-10): 10 → 15 (第 2 次扩容)
add(11): 15 → 22 (第 3 次扩容)
...
答案是:10 次添加,触发 2 次扩容。
4.3 Integer.MAX_VALUE 陷阱
ArrayList<int[]> list = new ArrayList<>();
try {
for (int i = 0; i < Integer.MAX_VALUE; i++) {
list.add(new int[1000]); // 每次分配 8KB
}
} catch (OutOfMemoryError e) {
// 在到达 MAX_ARRAY_SIZE 之前就会 OOM
// 因为数组最大只能到 Integer.MAX_VALUE - 8
}
【学习小结】
边界条件要点:
- 最大容量
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
- 扩容超过限制会抛出
OutOfMemoryError
- 注意整数溢出风险
五、生产避坑清单 🟡
5.1 常见问题代码
// ❌ 错误:循环内不预设容量
public List<User> getUsers(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
users.add(userService.getUser(id));
}
return users;
}
// ✅ 正确:预设容量(如果 ids 可预估大小)
public List<User> getUsers(List<Long> ids) {
List<User> users = new ArrayList<>(ids.size());
for (Long id : ids) {
users.add(userService.getUser(id));
}
return users;
}
5.2 GC 调优案例
我们的订单系统在重构前,GC 频繁停顿。排查后发现:
// 原来的代码
List<OrderItem> items = new ArrayList<>();
for (Order order : orders) {
items.addAll(order.getItems()); // 触发多次小扩容
}
优化后:
// 优化后:预估总大小
int totalSize = orders.stream()
.mapToInt(o -> o.getItems().size())
.sum();
List<OrderItem> items = new ArrayList<>(totalSize);
for (Order order : orders) {
items.addAll(order.getItems());
}
GC 停顿时间从 120ms 降到 15ms。
5.3 容量设置公式
// 经验公式:避免扩容的最优初始容量
int optimalCapacity = (int) (expectedSize / 0.75) + 1;
// 或者直接用 expectedSize + 1
List<T> list = new ArrayList<>(expectedSize + 1);
💡
预设容量时,可以适当多预留 10%~20% 的 buffer,防止估算偏差导致额外扩容。
六、ArrayList vs Vector 🟢
虽然 ArrayList 最常用,但 JDK 中还有另一个 List 实现——Vector:
// Vector 的扩容
private void grow(int capacity) {
int oldCapacity = capacity * 2; // 2 倍扩容
}
// ArrayList 的扩容
private void grow(int minCapacity) {
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍
}
⚠️
Vector 是 JDK 1.0 的遗留类,已不推荐使用。如果需要线程安全的 List,用 Collections.synchronizedList() 或 CopyOnWriteArrayList。
七、工程选型建议 🟡
7.1 选型决策树
graph TD
A[需要 List?] --> B{需要线程安全?}
B -->|是| C[数据量小?]
B -->|否| D[需要随机访问?]
D -->|否| E[考虑 LinkedList]
D -->|是| F[ArrayList 首选]
C -->|是| G[CopyOnWriteArrayList]
C -->|否| H[Collections.synchronizedList]
7.2 最佳实践
// 单线程:ArrayList 是 95% 场景的首选
List<String> list = new ArrayList<>(expectedSize);
// 读多写少:CopyOnWriteArrayList
List<String> readHeavy = new CopyOnWriteArrayList<>();
// 批量处理:用 addAll 代替循环 add
list.addAll(batchData);
// 内存敏感:trimToSize
list.trimToSize();
【学习小结】
工程选型要点:
- 单线程用 ArrayList,预设容量
- 多线程写多用 ConcurrentHashMap 或 synchronizedList
- 多线程读多写少用 CopyOnWriteArrayList
- 循环中添加必须预设容量
八、追问升级
8.1 第一层追问
面试官:"ArrayList 的 add(int index, E element) 方法插入元素的时间复杂度是多少?"
候选人:"O(1)?"
面试官:"为什么不是?"
正确回答:O(n)。因为需要将 index 之后的元素全部后移一位,时间复杂度是 O(n)。
8.2 第二层追问
面试官:"add(int index, E element) 和 add(E e) 有什么区别?"
候选人:...
正确回答:
add(E e):尾部添加,O(1) 均摊
add(int index, E e):中间插入,O(n)
8.3 第三层追问
面试官:"ArrayList 删除元素后,容量会缩小吗?"
候选人:...
正确回答:ArrayList 不会自动缩小容量。如果需要,用 trimToSize() 手动释放。
8.4 第四层追问
面试官:"ArrayList 的 iterator 是 fail-fast 的,什么意思?"
候选人:...
正确回答:fail-fast 意味着一旦检测到结构性修改(在迭代过程中调用 add/remove/clear),就会抛出 ConcurrentModificationException。这是检测并发修改的机制,不是线程安全保证。