ArrayList 底层原理
目标:面试里把 ArrayList 讲到"源码级",包含:数据结构、扩容机制、add/remove/get/set、fail-fast、迭代器、subList 坑、和 LinkedList/Vector/CopyOnWriteArrayList 对比,以及高频追问话术。
1. ArrayList 是什么?一句话定义
- 基于动态数组(Object[])实现的可变长线性表。
- 优点:随机访问快(O(1)),遍历快,内存连续、CPU cache 友好。
- 缺点:中间插入/删除慢(O(n)),扩容有成本,线程不安全。
2. 核心字段(源码必会)
2.1 关键字段
transient Object[] elementData;:底层数组(真正存放元素)private int size;:当前元素个数(不是数组长度)protected transient int modCount;(继承自 AbstractList):结构性修改次数,用于 fail-fast
2.2 两个"空数组"常量(JDK8 常见)
EMPTY_ELEMENTDATA:默认构造时用(容量 0)DEFAULTCAPACITY_EMPTY_ELEMENTDATA:区分"默认构造但还没真正分配容量"的状态
面试点:默认
new ArrayList<>()并不会立刻创建长度为 10 的数组,而是第一次 add 时才创建默认容量。
3. 构造函数行为(很容易被问)
3.1 new ArrayList()
elementData先指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)- 第一次 add 时,分配默认容量 10
3.2 new ArrayList(int initialCapacity)
initialCapacity > 0:直接分配对应长度数组initialCapacity == 0:使用EMPTY_ELEMENTDATA< 0:抛IllegalArgumentException
3.3 new ArrayList(Collection<? extends E> c)
- 直接
c.toArray()拷贝 - 注意:
toArray()返回Object[],如果不是Object[]会再Arrays.copyOf一次
4. add 流程(主干一定会)
4.1 add(E e)
核心步骤:
ensureCapacityInternal(size + 1):确保能放下新元素elementData[size++] = e:尾插modCount++(结构性修改)
时间复杂度:均摊 O(1)(amortized O(1)),扩容那次是 O(n)。
4.2 add(int index, E element)
核心步骤:
- 校验 index
- 扩容检查
System.arraycopy(...)把 index 之后整体右移一位- 放入新元素,
size++,modCount++
时间复杂度:O(n)(移动元素)。
5. 扩容机制(面试高频,必须说出 1.5 倍)
5.1 扩容触发
当 minCapacity > elementData.length 时扩容。
5.2 新容量计算(JDK8)
newCapacity = oldCapacity + (oldCapacity >> 1)- 等价于 1.5 倍扩容
并且:
- 若
newCapacity < minCapacity:直接用minCapacity - 若超过
MAX_ARRAY_SIZE(接近Integer.MAX_VALUE):走hugeCapacity处理,避免溢出
5.3 拷贝成本
扩容本质是:新建更大数组 + Arrays.copyOf(底层 System.arraycopy)把旧数组拷贝过去。
- 扩容那一次是 O(n)
- 但总体均摊下来,尾插仍然是均摊 O(1)
5.4 面试加分点:如何减少扩容?
- 你知道大概容量:用
new ArrayList<>(capacity)或ensureCapacity(capacity)。 - 场景:批量导入、分页聚合、预期固定条数的缓存等。
6. get / set / remove / clear(常问复杂度)
6.1 get(int index)
- 直接
elementData[index] - 时间复杂度:O(1)
6.2 set(int index, E element)
- 覆盖旧值,返回旧值
- 时间复杂度:O(1)
6.3 remove(int index)
步骤:
- 取旧值
System.arraycopy左移覆盖(index 之后整体左移一位)elementData[--size] = null(帮助 GC)modCount++
时间复杂度:O(n)
6.4 remove(Object o)
- 找到第一次匹配的元素(
equals)后,调用fastRemove(index)做左移 - 最坏:O(n)
6.5 clear()
- 把
[0..size)全部置null,size=0 - 时间复杂度:O(n)(要清引用)
7. fail-fast:为什么 foreach 删除会报 ConcurrentModificationException?(必考)
7.1 机制
- 迭代器创建时捕获
expectedModCount = modCount - 每次
next()/remove()都会检查modCount == expectedModCount - 如果你在迭代过程中,使用 list 的
add/remove直接改结构,modCount变了,迭代器就炸:ConcurrentModificationException
7.2 正确删除方式
- 用迭代器自己的
Iterator.remove()(会同步更新 expectedModCount) - 或者用
removeIf(...) - 或者倒序 for(按 index 删除)
面试一句话:fail-fast 是"尽快失败",不是并发安全保证。并发下它可能不抛,也可能抛。
8. subList 的坑(高级开发很爱问,踩过才懂)
8.1 subList 不是拷贝,是视图
subList(from, to) 返回的是 SubList 视图,底层仍引用原 list 的数组。
8.2 常见坑
- 结构性修改冲突:对原 list 做 add/remove,subList 再操作会触发 CME(因为 modCount 不一致)
- subList 转 ArrayList:
new ArrayList<>(list.subList(...))才是拷贝(安全)
记法:subList = view,不是副本。
9. 线程安全相关:ArrayList 为啥不安全?怎么解决?
9.1 典型并发问题
两个线程同时 add:
- 都读到相同的
size - 写入同一个 index
size++竞争导致丢数据或覆盖- 扩容时更容易出问题
9.2 解决方案(按场景)
- 读多写少:
CopyOnWriteArrayList - 简单互斥:
Collections.synchronizedList(new ArrayList<>()) - 更通用:外部加锁(
ReentrantLock)或用线程安全容器/队列
10. ArrayList vs LinkedList vs Vector vs CopyOnWriteArrayList(面试必比)
10.1 ArrayList vs LinkedList
- 随机访问:ArrayList O(1);LinkedList O(n)
- 中间插入删除:理论 LinkedList O(1)(已定位节点),但定位是 O(n),且对象分散、cache 不友好
- 遍历:ArrayList 通常更快(内存连续)
面试真相:大多数业务场景,ArrayList 更快,LinkedList 很少是最优。
10.2 ArrayList vs Vector
- Vector 方法级
synchronized:线程安全但性能差 - Vector 扩容策略:很多实现是 2 倍(也可通过构造参数指定增量)
- 现代基本不用 Vector
10.3 ArrayList vs CopyOnWriteArrayList
- 写:COW 每次写都会复制整个数组(O(n)),非常贵
- 读:无锁读取,迭代器是快照(不会 CME)
- 适合:读多写少、元素量不大且写不频繁(如配置、监听器列表)
11. 高频追问(直接背答案)
Q1:ArrayList 默认容量是多少?什么时候分配?
- 默认构造时容量是 0(空数组)
- 第一次 add 时分配默认容量 10(JDK8 常见)
Q2:扩容为什么是 1.5 倍?
- 比 2 倍更省内存;比固定增量更少扩容次数
- 1.5 倍是时间与空间的折中
Q3:为什么 remove 要把最后一个置 null?
- 解除引用,帮助 GC 回收对象,避免内存泄漏
Q4:foreach 删除为什么报 CME?
- fail-fast:迭代器的 expectedModCount 和 modCount 不一致
- 正确方式用 iterator.remove / removeIf / 倒序删除
Q5:subList 有啥坑?
- subList 是视图,和原 list 共享数组
- 原 list 结构性修改会导致 subList 操作 CME
- 要独立列表:
new ArrayList<>(subList)
12. 一段"面试口播模板"(建议你直接练)
ArrayList 底层是 Object[] 动态数组,size 表示元素个数。add 尾插先 ensureCapacity,容量不够就按 1.5 倍扩容并拷贝数组,所以尾插是均摊 O(1),但扩容那次是 O(n)。随机访问 get/set 是 O(1),中间插入删除需要 arraycopy 移动元素是 O(n)。它不是线程安全的;遍历时如果结构性修改会因为 modCount/expectedModCount 触发 fail-fast 的 ConcurrentModificationException。subList 返回的是视图不是拷贝,和原 list 共享数组,原 list 结构性修改会影响 subList。
13. 面试加分:你真的会用的建议
- 预估容量就提前设置,能少很多扩容拷贝成本(尤其在大列表聚合、批量导入)
- 删除大量元素:优先
removeIf或先过滤后新建列表(避免 n 次 arraycopy) - 并发读多写少:COW;并发写多:别硬上 list,考虑队列、分片、锁策略或其他数据结构
14. 源码关键方法(面试前扫一遍就稳)
add(E e)、add(int,E)ensureCapacityInternal、growremove(int)、fastRemoveiterator()、Itr.checkForComodificationsubList(SubList 的实现)