☰
Java集合-02-ArrayList源码:扩容、System.arraycopy 与 fail-fast
2026/9/30 7:23:11 网站建设 项目流程

1. 结论先行:ArrayList 本质是动态数组

ArrayList 是 Java 集合框架中最常用的 List 实现之一,其底层本质是一个可动态扩容的对象数组。它之所以查询快、增删慢,根源就在于这个数组结构:数组支持按下标 O(1) 随机访问,但中间插入和删除需要整体挪动元素。

一句话总结:ArrayList = 数组 + 扩容机制 + 迭代器保护机制。理解这三个部分,就理解了 ArrayList 的核心。

本文从源码角度拆解 ArrayList 的扩容、System.arraycopy 挪位和 fail-fast 机制,并配图说明,帮助你把源码读透。

2. 核心字段

先看 ArrayList 的几个关键字段,它们是理解后续所有逻辑的基础。

// 默认初始容量 private static final int DEFAULT_CAPACITY = 10; // 空数组(无参构造时使用) private static final Object[] EMPTY_ELEMENTDATA = {}; // 默认容量空数组(懒加载时使用) private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 真正存储元素的数组 transient Object[] elementData; // 元素个数 private int size; // 结构性修改次数(fail-fast 核心) protected transient int modCount = 0;

这里有两个容易混淆的空数组:EMPTY_ELEMENTDATA用于指定容量为 0 的构造,DEFAULTCAPACITY_EMPTY_ELEMENTDATA用于无参构造。两者的区别在于:无参构造的数组在第一次 add 时会扩容到默认容量 10,而指定容量 0 的数组则按 0 容量起步。

3. 构造方法

ArrayList 提供了三个构造方法,分别对应不同的初始化场景。

3.1 无参构造

public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

无参构造只是把 elementData 指向一个空数组,并没有真正分配 10 个容量的空间。这就是懒加载:容量 10 的数组在第一次 add 时才真正创建。

3.2 指定容量构造

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

指定容量大于 0 时,直接创建对应大小的数组;等于 0 时使用空数组;小于 0 时抛出异常。

3.3 传入集合构造

public ArrayList(Collection<? extends E> c) { Object[] a = c.toArray(); if ((size = a.length) != 0) { if (c.getClass() == ArrayList.class) { elementData = a; } else { elementData = Arrays.copyOf(a, size, Object[].class); } } else { elementData = EMPTY_ELEMENTDATA; } }

传入集合时,直接把集合元素拷贝到新数组。如果传入的本身就是 ArrayList,则直接复用其内部数组(不复制元素),否则通过 Arrays.copyOf 复制。

4. 添加元素

添加元素是 ArrayList 最核心的操作之一,分为尾部追加和指定位置插入两种。

4.1 add(E):尾部追加

public boolean add(E e) { ensureCapacityInternal(size + 1); // 确保容量足够 elementData[size++] = e; // 赋值并 size++ return true; }

尾部追加的逻辑很简单:先确保容量够用,然后在下标 size 处赋值,最后 size 自增。整个过程是 O(1) 摊还复杂度。

4.2 add(int, E):指定位置插入

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

指定位置插入需要先把 index 之后的元素整体后移一位,再在 index 处赋值。这个挪位操作是 O(n) 的,正是 ArrayList 中间插入慢的根本原因。

下面用图说明 System.arraycopy 的挪位过程:

flowchart LR A["原数组: [A, B, C, D, E]"] -- "在 index=2 插入 X" --> B["System.arraycopy 把 C,D,E 后移"] B --> C["后移结果: [A, B, C, C, D, E]"] C -- "在 index=2 赋值 X" --> D["最终: [A, B, X, C, D, E]"]

5. 扩容机制

扩容是 ArrayList 最值得深入的部分,也是面试高频考点。

5.1 懒加载:第一次 add 才扩到 10

无参构造创建的 ArrayList 初始指向空数组,第一次 add 时才真正分配容量 10 的数组。这就是懒加载,也是面试中容易踩坑的点。

private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 结构性修改计数 if (minCapacity - elementData.length > 0) { grow(minCapacity); } }

当 elementData 还是默认空数组时,minCapacity 会被提升到 DEFAULT_CAPACITY(10),从而在第一次 add 时扩容到 10。

5.2 grow():1.5 倍扩容

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

扩容的核心公式是newCapacity = oldCapacity + (oldCapacity >> 1),即每次扩容为原来的 1.5 倍。例如 10 扩容到 15,15 扩容到 22,22 扩容到 33。

下面用图说明 1.5 倍扩容过程:

flowchart LR A["容量 10 已用 10"] -- "add 第 11 个元素" --> B["grow() 计算 newCapacity = 10 + 5 = 15"] B -- "Arrays.copyOf 拷贝" --> C["新数组容量 15 旧元素全部搬入"] C -- "继续 add" --> D["容量 15 用满后 再扩到 22"]

5.3 MAX_ARRAY_SIZE 与 OutOfMemoryError

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) { throw new OutOfMemoryError(); // 溢出 } return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }

当扩容后的容量超过 MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)时,会尝试使用更大的容量;如果 minCapacity 已经溢出为负数,则抛出 OutOfMemoryError。MAX_ARRAY_SIZE 预留 8 个位置是为了容纳对象头等 JVM 开销。

6. 删除元素

删除元素同样涉及数组挪位,是 O(n) 操作。

6.1 remove(int):按下标删除

public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { // 把 index 之后的元素整体前移一位 System.arraycopy(elementData, index + 1, elementData, index, numMoved); } elementData[--size] = null; // 置空,帮助 GC return oldValue; }

按下标删除时,把 index 之后的元素整体前移一位,然后把最后一个位置置空并 size 减一。置空操作是为了让 GC 可以回收不再引用的对象。

6.2 remove(Object):遍历查找后删除

public boolean remove(Object o) { if (o == null) { for (int index = 0; index < size; index++) { if (elementData[index] == null) { fastRemove(index); return true; } } } else { for (int index = 0; index < size; index++) { if (o.equals(elementData[index])) { fastRemove(index); return true; } } } return false; }

按对象删除时,先遍历数组找到目标元素,再调用 fastRemove 删除。这里用 equals 比较,所以自定义对象需要正确重写 equals 方法。

6.3 fastRemove:跳过越界检查的快速删除

private void fastRemove(int index) { modCount++; int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(elementData, index + 1, elementData, index, numMoved); } elementData[--size] = null; }

fastRemove 与 remove(int) 逻辑相同,只是跳过了越界检查,因为调用方已经确认 index 合法。

7. 查询与修改

查询和修改是 ArrayList 的优势所在,因为数组支持按下标随机访问。

public E get(int index) { rangeCheck(index); return elementData(index); // 直接按下标取 } public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; }

get 和 set 都是直接通过下标访问数组元素,时间复杂度为 O(1)。这正是 ArrayList 查询快的原因:不需要像链表那样从头遍历。

8. 迭代器与 fail-fast

迭代器是 ArrayList 中另一个高频考点,尤其是 fail-fast 机制。

8.1 Itr 的核心字段

private class Itr implements Iterator<E> { int cursor; // 下一个要返回的元素下标 int lastRet = -1; // 上一次返回的元素下标,-1 表示没有 int expectedModCount = modCount; // 期望的结构修改次数 }

Itr 维护三个关键字段:cursor 记录下一个元素位置,lastRet 记录上一次返回位置,expectedModCount 记录创建迭代器时的 modCount。

8.2 checkForComodification:fail-fast 核心

final void checkForComodification() { if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }

每次调用 next() 或 remove() 时都会检查 modCount 是否等于 expectedModCount。如果期间发生了结构性修改(如 add、remove、clear),modCount 会变化,从而抛出 ConcurrentModificationException。

下面用流程图说明 fail-fast 机制:

flowchart TD A["创建迭代器 expectedModCount = modCount"] --> B["调用 next()"] B --> C{"modCount == expectedModCount?"} C -- "是" --> D["正常返回元素"] C -- "否" --> E["抛出 ConcurrentModificationException"] D --> B

8.3 为什么 for-each 中删除会抛异常

for-each 底层就是使用迭代器遍历。如果在遍历过程中调用 list.remove(),会修改 modCount,导致迭代器检测到 modCount 与 expectedModCount 不一致,从而抛出 ConcurrentModificationException。

// 这段代码会抛 ConcurrentModificationException for (String s : list) { if (s.equals("b")) { list.remove(s); // 直接调用 list.remove,modCount 变化 } }

8.4 正确删除方式

正确的删除方式有两种:使用 Iterator.remove() 或使用 removeIf。

// 方式一:Iterator.remove() Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); if (s.equals("b")) { it.remove(); // 会同步更新 expectedModCount } } // 方式二:removeIf(JDK 8+) list.removeIf(s -> s.equals("b"));

Iterator.remove() 之所以安全,是因为它在删除后会同步更新 expectedModCount,保持与 modCount 一致。removeIf 内部也做了同样的处理。

9. subList 视图坑

subList 返回的是原 List 的视图,而不是独立副本,这是一个容易踩坑的地方。

List<String> sub = list.subList(0, 3); // 对 sub 的任何结构性修改都会反映到原 list 上 sub.add("x"); // 原 list 也会多一个元素

更危险的是:如果 subList 创建后,原 list 发生了结构性修改,再操作 subList 会抛出 ConcurrentModificationException。因为 subList 内部也维护了 expectedModCount。

10. 复杂度总结与使用场景

下表总结了 ArrayList 各操作的时间复杂度:

操作时间复杂度说明
get(index)O(1)数组按下标随机访问
set(index, e)O(1)数组按下标赋值
add(e) 尾部追加O(1) 摊还扩容时 O(n),但均摊 O(1)
add(index, e)O(n)需要挪动元素
remove(index)O(n)需要挪动元素
remove(Object)O(n)先遍历查找再挪位
contains(Object)O(n)线性遍历

适用场景:频繁按下标查询、尾部增删、元素数量可预估的场景。

不适用场景:频繁在中间插入/删除、需要频繁按值查找的场景(此时应考虑 LinkedList 或 HashMap)。

11. 面试题速答

最后整理几个高频面试题,帮助快速复习。

Q1:默认容量是多少?什么时候初始化?

默认容量是 10,但无参构造并不会立即创建容

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询