ArrayList 实现原理深度剖析
——动态数组的“三十六计”,从扩容心机到内存暗战
一、底层架构:数组的“变形记”
ArrayList 的核心是一个会“自我膨胀”的数组,其设计哲学可以概括为:用空间换时间,以冗余换自由。
1. 核心成员变量
// 真正存储数据的数组(用 transient 修饰,序列化时有特殊处理)
transient Object[] elementData;
// 当前元素数量(注意:不是数组长度!)
private int size;
关键差异:
- 容量(capacity):
elementData.length(数组的实际长度,像杯子的容积)。 - 大小(size):当前存储的元素数量(杯中实际水量)。
初始容量默认10,但构造空列表时(JDK8+),数组延迟到首次添加元素时才创建(内存优化小技巧)。
2. 构造方法里的“小心机”
-
无参构造:
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; // 指向空数组 }首次添加元素时,才扩容到默认容量10(避免内存浪费)。
-
指定容量构造:
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 的“生存智慧”
扩容是 ArrayList 的看家本领,其过程堪比“乾坤大挪移”。
1. 扩容触发条件
// add() 方法中的扩容检查
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e;
return true;
}
- 核心逻辑:当
size + 1 > elementData.length时触发扩容(新元素无处安放)。
2. 扩容公式:1.5倍增长的数学魔法
// 计算新容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 右移1位相当于除以2
- 示例:
- 旧容量10 → 新容量15(10 + 5)
- 旧容量15 → 新容量22(15 + 7)
设计意图:平衡扩容次数与空间浪费(2倍太激进,1.2倍又太频繁)。
3. 扩容全流程
- 计算新容量:取
旧容量 × 1.5和所需最小容量的较大值。 - 创建新数组:
Arrays.copyOf()或System.arraycopy()拷贝数据。 - 旧数组退役:旧数组被 GC 回收(像极了一代代被淘汰的手机)。
扩容代码片段:
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);
}
4. 扩容性能开销
- 时间复杂度:O(n)(需要拷贝所有元素)。
- 空间开销:旧数组内存被短暂同时占用(内存敏感场景需警惕)。
避坑技巧:
// 预先设置足够容量,避免多次扩容
List<Integer> list = new ArrayList<>(1000000); // 一次性吃饱,拒绝反复横跳
三、元素操作:数组的“刀光剑影”
1. 添加元素
- 尾部追加:O(1)(直接放到数组末尾)。
- 中间插入:O(n)(需要移动后续元素,像早高峰插队引发众怒)。
public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // 关键代码:将 index 后的元素右移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
2. 删除元素
- 尾部删除:O(1)(直接 size--)。
- 中间删除:O(n)(需要移动后续元素补位)。
public E remove(int index) { rangeCheck(index); E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // 置空帮助 GC return oldValue; }
3. 随机访问
// 直接通过索引访问数组,时间复杂度 O(1)
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
性能优势:比 LinkedList 快 N 倍(链表需要从头遍历)。
四、线程安全:ArrayList 的“阿喀琉斯之踵”
1. 多线程操作风险
- 数据覆盖:两个线程同时执行 add(),导致元素丢失。
- 数组越界:扩容过程中 size 未及时更新,引发
ArrayIndexOutOfBoundsException。
2. 故障复现代码
List<String> list = new ArrayList<>();
// 两个线程同时执行 1000 次 add()
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
list.add("test");
}
};
new Thread(task).start();
new Thread(task).start();
// 最终 size 可能小于 2000(部分数据被覆盖)
3. 解决方案
- 加锁:使用
Collections.synchronizedList(new ArrayList<>())。 - 写时复制:使用
CopyOnWriteArrayList(适合读多写少场景)。
五、序列化策略:空间优化的“障眼法”
1. 为什么 elementData 用 transient 修饰?
- 避免序列化空位:默认序列化会写入整个数组(包括未使用的空槽),浪费空间。
2. 自定义序列化
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
// 只序列化实际存储的元素
s.defaultWriteObject();
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
}
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
// 反序列化时重建数组
elementData = EMPTY_ELEMENTDATA;
s.defaultReadObject();
s.readInt(); // ignored
// 根据 size 重建数组
}
效果:序列化后的数据量减少(像压缩行李箱,只带必需品)。
六、迭代器设计:fail-fast 机制的“无间道”
1. modCount 的使命
- 修改计数器:记录列表结构修改次数(add/remove 等操作会递增)。
2. 迭代过程中的安全检查
// ArrayList.Itr 迭代器源码
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
经典错误:
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
for (String s : list) {
if (s.equals("B")) list.remove(s); // 抛出 ConcurrentModificationException
}
正确做法:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().equals("B")) it.remove(); // 安全删除
}
七、性能优化:ArrayList 的“内功心法”
1. 预分配容量
- 直接设置初始容量:
new ArrayList<>(N),避免多次扩容。 - 提前扩容:
ensureCapacity(int minCapacity)预先扩容。
2. 批量操作优先
- 用 addAll() 替代循环 add():减少扩容次数。
- 用 clear() 替代循环 remove():直接置空数组(O(1) vs O(n))。
3. 及时释放空间
// 释放未使用的容量(像减肥后扔掉大号衣服)
list.trimToSize();
八、终极思考:为什么 ArrayList 不是完美无缺的?
- 增删效率低:数据搬运成本高(适合读多写少场景)。
- 内存碎片风险:频繁扩容可能引发 Full GC。
- 线程安全代价:同步锁或写时复制会降低性能。
总结:
ArrayList 是动态数组的典范之作,其精妙的设计在空间与时间、性能与安全之间找到了优雅的平衡。理解其原理不仅是面试通关的钥匙,更是写出高性能代码的基石。下次当你写下 new ArrayList<>() 时,请记住——这行简单的代码背后,藏着一场数组与算法的华丽共舞!
彩蛋:
若面试官问“为什么 ArrayList 的迭代器比 for 循环快?”,请回答:“因为迭代器直接访问数组,而 for 循环每次调用 get() 要检查索引范围!”(真实原因:get() 方法中有 rangeCheck(index) 检查)。