一、核心设计思想
-
动态数组本质
-
底层通过
Object[] elementData
数组存储数据 -
默认构造空数组:
javapublic ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 共享空数组 }
-
-
扩容目标
- 空间换时间:以 1.5 倍系数扩容,平衡内存占用与扩容频率
- 自动扩容:无需手动管理容量,但需注意性能损耗
二、触发扩容的精确时刻
操作类型 | 触发条件 | 示例场景 |
---|---|---|
add(E e) |
size == elementData.length |
添加第 11 个元素到默认容量 10 的列表 |
addAll(Collection) |
size + 新增元素数 > elementData.length |
批量添加 20 个元素到容量 15 的列表 |
ensureCapacity() |
显式设置的容量 > 当前容量 | 提前扩容到 1000 以避免后续多次扩容 |
三、JDK11 源码级扩容流程
1. 入口方法调用链
java
add() → add(e, elementData, size) → grow() → grow(int minCapacity)
2. 核心源码解析
java
// JDK11
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5倍计算
// 处理特殊场景
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 首次扩容
return Math.max(DEFAULT_CAPACITY, minCapacity); // 默认10
}
if (minCapacity < 0) throw new OutOfMemoryError(); // 溢出检查
return minCapacity;
}
// 处理最大容量限制
return (newCapacity - MAX_ARRAY_SIZE <= 0) ?
newCapacity : hugeCapacity(minCapacity);
}
private static int hugeCapacity(int minCapacity) {
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
3. 扩容步骤详解
-
容量计算
-
基础扩容 :
新容量 = 旧容量 × 1.5
(位运算优化为oldCapacity + (oldCapacity >> 1)
) -
特殊情况:
- 首次扩容:取
max(10, 需求容量)
- 扩容不足时:直接使用需求容量
- 超大容量:最大为
Integer.MAX_VALUE
- 首次扩容:取
-
-
数据迁移
- 通过
Arrays.copyOf()
创建新数组 - 底层调用本地方法
System.arraycopy()
实现高效复制
- 通过
四、扩容性能特征
操作类型 | 时间复杂度 | 说明 |
---|---|---|
普通添加 | O(1) | 无需扩容时的常规操作 |
扩容时的添加 | O(n) | 需复制 n 个元素到新数组 |
批量添加(addAll) | O(n+m) | n=原数组长度,m=新增元素数量 |
性能对比测试(百万级元素):
java
// 测试代码片段
public class ArrayListTest {
public static void main(String[] args) {
int size = 1000000;
// 测试默认构造
long t1 = System.nanoTime();
List<Integer> defaultList = new ArrayList<>();
for (int i = 0; i < size; i++) {
defaultList.add(i);
}
System.out.println("默认构造耗时:" + (System.nanoTime()-t1)/1000000 + "ms");
// 测试预设容量
long t2 = System.nanoTime();
List<Integer> preSizedList = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
preSizedList.add(i);
}
System.out.println("预设容量耗时:" + (System.nanoTime()-t2)/1000000 + "ms");
}
}
初始容量 | 耗时(100万次 add) |
---|---|
默认 10 | 35 ms |
1,000,000 | 12 ms |
五、扩容过程推演
场景示例:默认构造连续添加元素
java
List<String> list = new ArrayList<>();
for (int i=0; i<16; i++) list.add("item");
-
首次扩容(添加第1个元素)
- 空数组 → 扩容到 10
-
常规扩容
添加元素序号 触发扩容时容量 新容量计算 10 10 → 15 10 + (10>>1) = 15 16 15 → 22 15 + (15>>1) = 22 23 22 → 33 22 + (22>>1) = 33
六、设计哲学与工程权衡
-
1.5 倍系数的数学原理
- 分摊分析:经过 n 次插入操作,总时间复杂度为 O(n)
- 空间利用率:每次扩容后剩余空间逐渐增大,减少扩容频率
七、开发最佳实践
-
容量预设
java// 已知要存储 50000 个用户 List<User> users = new ArrayList<>(50000 + 1000); // 增加安全余量
-
避免陷阱
-
场景 1:嵌套循环中的 add 操作
java// 错误示例:每次外层循环都新建 ArrayList for (int i=0; i<1000; i++) { List<Data> tempList = new ArrayList<>(); // 应复用 list // ... 添加操作 }
-
场景 2:超大对象存储
java// 建议分块存储 List<byte[]> chunks = new ArrayList<>(100); for (int i=0; i<100; i++) { chunks.add(new byte[1_000_000]); // 每个元素占 1MB } // 总内存占用:100MB × 1.5 = 150MB
-
-
线程安全方案
java// 方案 1:同步包装 List<String> safeList = Collections.synchronizedList(new ArrayList<>()); // 方案 2:写时复制 CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
八、高频面试题解析
问题 1:ArrayList 和 LinkedList 如何选择?
-
ArrayList 优势:
- 随机访问 O(1)
- 内存连续,CPU 缓存友好
-
LinkedList 适用场景:
- 频繁在任意位置插入/删除
- 不需要随机访问
问题 2:如何实现安全缩容?
java
// 手动缩容到实际大小
list.trimToSize(); // 将容量调整为当前 size
// 注意:频繁调用会导致内存抖动
问题 3:为什么 elementData 用 transient 修饰?
- 序列化优化 :ArrayList 自定义了
writeObject
/readObject
方法,只序列化实际存储的元素(跳过空余位置),减少传输数据量。
九、总结
理解 ArrayList 扩容机制的意义:
- 性能优化:通过预设容量避免多次扩容
- 内存管理:预估超大集合的内存占用
- 设计启示:学习空间换时间的经典实现