# ArrayList 深度分析
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
基于JDK11进行分析, 大部分公司已经开始使用JDK11
ArrayList 是 Java 集合中使用最频繁的类之一,但我们在开发中常常只停留在它的使用层面。深入分析其源码,能帮助我们理解底层数据结构与扩容机制,避免因误用导致的性能问题或并发异常。
掌握这些实现细节,不仅能在复杂业务中做出更合适的集合选择,还能提升我们对 Java 内存模型、异常机制和 API 设计的整体认知。对高级研发来说,源码不是为了背诵,而是为了看清系统运行的本质,做出更精准的工程判断。
# 一、继承体系
继承结构
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2
| 父类/接口 | 作用 | 责任 |
|---|---|---|
AbstractList | 抽象骨架 | 复用大部分 List 实现逻辑 |
List | 行为规范 | 定义有序集合的基本操作 |
RandomAccess | 标记接口 | 指示支持快速随机访问 |
Cloneable | 接口能力 | 支持浅拷贝(clone()) |
Serializable | 接口能力 | 支持对象序列化(跨 JVM 传输) |
# 二、底层数据结构解析
ArrayList 是典型的基于数组实现的动态顺序容器,其源码实现虽然简洁,但蕴含着 Java 集合框架的多个设计哲学。本章将从底层字段、扩容机制、内存管理及 fail-fast 原理几方面深入解析。
本章节以原理结构为主, 具体源码解析可查看核心方法源码解析
# 核心字段
transient Object[] elementData;
这是 ArrayList 用于实际存储元素的数组:
Object[]:为了支持泛型,底层以Object类型数组存储所有元素。transient:表示该字段不会被默认序列化(自定义序列化逻辑时手动处理)。这样设计是为了在序列化时只序列化实际元素,避免浪费空间。
此外还有:
private int size; // 当前有效元素个数
elementData.length 代表容量,而 size 才是实际元素数量。
# 默认容量
源码中默认容量给了一个10
private static final int DEFAULT_CAPACITY = 10;
使用无参构造函数创建一个 ArrayList:
List<String> list = new ArrayList<>();
它在初始化时并不会立刻分配容量,而是使用一个共享的空数组:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
2
3
4
5
6
真正分配初始容量(默认值 10)是在第一次 add() 元素时才触发
设计理由:
- 避免创建未使用的数组(典型的懒加载策略)
- 如果创建了很多未使用的
ArrayList实例,可以节省内存
# 扩容机制
ArrayList 采用懒初始化,在首次调用 add() 时分配默认容量(10)。当内部数组容量不足以容纳新增元素时,会自动触发扩容,扩容入口是 grow() 方法:
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 进行扩容
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
2
3
4
5
6
7
8
9
10
11
- 采用 1.5倍扩容策略,兼顾性能和内存利用,避免频繁扩容带来的性能开销。
Arrays.copyOf本质调用 JVM 原生的System.arraycopy,高效完成内存拷贝。- 扩容时还考虑了最大数组长度限制,防止溢出导致异常。
**扩容示意:**当添加第11个元素时,容量不足(默认10),扩容为15,触发数组复制。
JDK11的grow()相对于JDK8, 有着更好的封装, 没有那么多冗余代码, 核心逻辑类似
# GC 视角下的扩容问题
频繁扩容带来的问题:
- 每次扩容都会创建新数组 → 旧数组等待 GC 回收
- 如果列表体量大,频繁 arraycopy 会增加 GC 压力
- 尤其在大数据处理、长列表处理时,建议预设初始容量:
List<User> list = new ArrayList<>(expectedSize);
还可通过:
list.ensureCapacity(expectedSize);
显式控制容量,避免扩容。
# fail-fast 的核心机制
protected transient int modCount = 0;
modCount是用于记录结构性修改次数的字段(如add/remove/clear等操作)- 所有迭代器(
Iterator、ListIterator)在创建时会捕获当前的modCount快照
在迭代过程中,如果发现实际 modCount ≠ 快照 expectedModCount,则触发:
throw new ConcurrentModificationException();
这就是 Java 集合中的fail-fast 机制。
目的:
- 快速发现并发修改问题
- 不是线程安全的设计,而是一种“快速失败”的调试工具
# 三、核心方法解析(JDK11)
# 增
add(E e)
public boolean add(E e) {
// 为什么在增强for循环以及迭代器中不建议修改元素 并发会导致modCount大小不一致
modCount++;
add(e, elementData, size);
return true;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
这里
add分两步:先修改modCount,再调用带参数的私有add方法。add方法判断容量是否已满,满了就调用grow()扩容(返回新数组)。扩容后重新赋值给
elementData,保证底层数组引用正确。通过传入当前数组和大小,避免不必要的字段访问,提高灵活性。
# 扩容
grow()
JDK11封装的动态扩容方法:
private Object[] grow() {
return grow(size + 1);
}
private Object[] grow(int minCapacity) {
return elementData = Arrays.copyOf(elementData, newCapacity(minCapacity));
}
private int newCapacity(int minCapacity) {
int oldCapacity = elementData.length;
// 扩容 1.5 倍策略
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 当扩容后的容量仍不足时,处理特殊情况
if (newCapacity - minCapacity <= 0) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
return Math.max(DEFAULT_CAPACITY, minCapacity);
if (minCapacity < 0) // 容量溢出抛出异常
throw new OutOfMemoryError();
return minCapacity;
}
// 超过最大数组限制,调用 hugeCapacity 处理
return (newCapacity - MAX_ARRAY_SIZE <= 0)
? newCapacity
: hugeCapacity(minCapacity);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
grow()调用无参版本,默认扩容到size + 1(确保至少能容纳新元素)。grow(int minCapacity)方法通过调用newCapacity(minCapacity)计算新容量,最终使用Arrays.copyOf复制数组并返回。newCapacity是扩容策略核心:基础扩容为旧容量的 1.5 倍 (
oldCapacity + oldCapacity >> 1)。如果扩容后仍不足以容纳新元素,处理两种特殊情况:
- 如果当前数组是默认的空数组(懒初始化的情况),则扩容为默认容量或
minCapacity中较大者。 - 如果容量出现溢出,抛出
OutOfMemoryError。
- 如果当前数组是默认的空数组(懒初始化的情况),则扩容为默认容量或
若新容量超过最大数组限制,调用
hugeCapacity返回一个安全的最大容量。
使用 Arrays.copyOf 进行底层数组复制,调用的是高性能的 System.arraycopy。
# 删
remove(int index)
public E remove(int index) {
// 检查索引是否越界,size为当前元素数量
Objects.checkIndex(index, size);
final Object[] es = elementData;
@SuppressWarnings("unchecked")
E oldValue = (E) es[index];
fastRemove(es, index);
return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
// 将删除元素后的后续元素整体左移一位
System.arraycopy(es, i + 1, es, i, newSize - i);
// 将最后一个元素置为 null,帮助 GC 回收
es[size = newSize] = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
remove(int index)首先调用Objects.checkIndex方法进行边界检查,避免访问非法索引。获取当前底层数组
elementData的引用,减少字段访问开销。保存被删除元素的旧值,以便返回。
调用
fastRemove方法实现快速删除逻辑。fastRemove内部:modCount++,结构修改计数器递增,支持 fail-fast 机制。计算新容量
newSize,并判断是否需要移动元素:- 如果删除元素不是最后一个,调用
System.arraycopy将后续元素统一左移一位。
- 如果删除元素不是最后一个,调用
将最后一个元素位置赋值为
null,解除引用,避免内存泄漏。更新
size字段,反映集合当前大小。
# 查
get(int index)
public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}
2
3
4
- 索引边界检查,避免越界异常。
- 直接访问底层数组,性能高效。(O(1))
# modCount说明
- 定义在
AbstractList中,用于检测迭代过程中集合结构变化,触发ConcurrentModificationException,实现 fail-fast 机制。
┌────────────┐
│ 开始 │
└────┬───────┘
│
▼
┌───────────────────────────────┐
│ 创建 ArrayList 并执行结构性操作 │
│ (add/remove/clear 等) │
│ → modCount 增加 │
└────┬──────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 创建 Iterator │
│ → expectedModCount = modCount │
└────┬──────────────────────────┘
│
▼
┌───────────────────────────────┐
│ 调用 iterator.next() │
│ → 比较 modCount 与 expectedModCount │
└────┬──────────────────────────┘
│
▼
┌──────────────┐ modCount == expectedModCount ┌──────────────────────────────┐
│ 继续迭代下一项 │◄──────────────────────────────┤ 条件判断:一致,继续正常迭代 │
└──────────────┘ └──────────────────────────────┘
│
│ 否
▼
┌──────────────────────────────┐
│ 结构被外部修改 │
│ → modCount 发生变化 │
│ → 与 expectedModCount 不一致 │
└────┬─────────────────────────┘
▼
┌──────────────────────────────┐
│ 抛出 ConcurrentModificationException │
└──────────────────────────────┘
│
▼
┌────────────┐
│ 结束 │
└────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# 四、并发场景下的问题
# ArrayList 线程不安全
ArrayList底层是基于数组实现,内部没有任何同步机制。- 多线程环境下同时对
ArrayList进行修改(如add、remove)会导致数据结构状态不一致。 modCount字段用于记录结构性修改次数(增删元素时自增)。modCount不是同步变量,且没有加锁,在多线程并发操作时会出现竞态条件。
# ConcurrentModificationException
- 该异常通常在迭代器遍历期间检测到结构修改时抛出,称为 fail-fast 机制。
- 迭代器内部保存了创建时的
modCount值。 - 迭代过程中如果
modCount发生变化(其他线程或代码修改了集合结构),迭代器会检测到不一致,立即抛出ConcurrentModificationException。 - 这是为了快速暴露并发修改错误,避免出现数据不一致或不可预测行为。
# 读写并发风险
- 写操作未同步:多个线程同时写会导致数组覆盖、size 不准确、数据丢失等严重问题。
- 读写冲突:写线程修改
elementData或size,读线程可能读到半更新状态,导致读取脏数据或异常。 - 迭代时并发修改:触发 fail-fast 异常,但 fail-fast 机制不能保证绝对安全,仍存在竞态。
- 内存可见性问题:
modCount和size无同步保证,可能导致线程间状态不同步。
# 解决方案
| 方案 | 特点 | 适用场景 |
|---|---|---|
Collections.synchronizedList() | 对所有方法加同步锁,保证线程安全,但性能较低,迭代时需手动同步 | 需要简单线程安全,写操作频繁但数据量不大 |
CopyOnWriteArrayList | 写操作复制数组,读操作无锁,读性能极高,写性能较低 | 读多写少场景,避免迭代时 ConcurrentModificationException |
| 使用外部锁同步 | 自定义使用 ReentrantLock 或同步代码块保证线程安全 | 复杂业务逻辑需要灵活控制同步粒度 |
| 完全避免共享修改 | 使用不可变集合或通过消息传递避免共享状态 | 高并发读写分离,追求极致性能 |