# 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
1
2
父类/接口 作用 责任
AbstractList 抽象骨架 复用大部分 List 实现逻辑
List 行为规范 定义有序集合的基本操作
RandomAccess 标记接口 指示支持快速随机访问
Cloneable 接口能力 支持浅拷贝(clone()
Serializable 接口能力 支持对象序列化(跨 JVM 传输)

# 二、底层数据结构解析

ArrayList 是典型的基于数组实现的动态顺序容器,其源码实现虽然简洁,但蕴含着 Java 集合框架的多个设计哲学。本章将从底层字段、扩容机制、内存管理及 fail-fast 原理几方面深入解析。

本章节以原理结构为主, 具体源码解析可查看核心方法源码解析

# 核心字段

transient Object[] elementData;
1

这是 ArrayList 用于实际存储元素的数组:

  • Object[]:为了支持泛型,底层以 Object 类型数组存储所有元素。
  • transient:表示该字段不会被默认序列化(自定义序列化逻辑时手动处理)。这样设计是为了在序列化时只序列化实际元素,避免浪费空间。

此外还有:

private int size; // 当前有效元素个数
1

elementData.length 代表容量,而 size 才是实际元素数量。

# 默认容量

源码中默认容量给了一个10

private static final int DEFAULT_CAPACITY = 10;
1

使用无参构造函数创建一个 ArrayList

List<String> list = new ArrayList<>();
1

它在初始化时并不会立刻分配容量,而是使用一个共享的空数组:

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 无参构造函数
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
1
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);
}
1
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);
1

还可通过:

list.ensureCapacity(expectedSize);
1

显式控制容量,避免扩容。


# fail-fast 的核心机制

protected transient int modCount = 0;
1
  • modCount 是用于记录结构性修改次数的字段(如 add/remove/clear 等操作)
  • 所有迭代器(IteratorListIterator)在创建时会捕获当前的 modCount 快照

在迭代过程中,如果发现实际 modCount ≠ 快照 expectedModCount,则触发:

throw new ConcurrentModificationException();
1

这就是 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;
}
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);
}

1
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;
}

1
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);
}
1
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 │
└──────────────────────────────┘
     │
     ▼
┌────────────┐
│   结束     │
└────────────┘

1
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 进行修改(如 addremove)会导致数据结构状态不一致。
  • modCount 字段用于记录结构性修改次数(增删元素时自增)。
  • modCount 不是同步变量,且没有加锁,在多线程并发操作时会出现竞态条件。

# ConcurrentModificationException

  • 该异常通常在迭代器遍历期间检测到结构修改时抛出,称为 fail-fast 机制
  • 迭代器内部保存了创建时的 modCount 值。
  • 迭代过程中如果 modCount 发生变化(其他线程或代码修改了集合结构),迭代器会检测到不一致,立即抛出 ConcurrentModificationException
  • 这是为了快速暴露并发修改错误,避免出现数据不一致或不可预测行为。

# 读写并发风险

  • 写操作未同步:多个线程同时写会导致数组覆盖、size 不准确、数据丢失等严重问题。
  • 读写冲突:写线程修改 elementDatasize,读线程可能读到半更新状态,导致读取脏数据或异常。
  • 迭代时并发修改:触发 fail-fast 异常,但 fail-fast 机制不能保证绝对安全,仍存在竞态。
  • 内存可见性问题modCountsize 无同步保证,可能导致线程间状态不同步。

# 解决方案

方案 特点 适用场景
Collections.synchronizedList() 对所有方法加同步锁,保证线程安全,但性能较低,迭代时需手动同步 需要简单线程安全,写操作频繁但数据量不大
CopyOnWriteArrayList 写操作复制数组,读操作无锁,读性能极高,写性能较低 读多写少场景,避免迭代时 ConcurrentModificationException
使用外部锁同步 自定义使用 ReentrantLock 或同步代码块保证线程安全 复杂业务逻辑需要灵活控制同步粒度
完全避免共享修改 使用不可变集合或通过消息传递避免共享状态 高并发读写分离,追求极致性能