集合-List-ArrayList

全文组织架构

ArrayList 的本质是一个由 Object[] 数组支撑、通过 System.arraycopy 完成元素迁移、在容量不足时自动 1.5 倍扩容的有序列表。O(1) 随机访问与连续内存带来的遍历性能是其核心优势,但操作的非原子性使其线程不安全,modCount 快照机制则让迭代能快速失败。实现层面,延迟分配、自定义序列化、fail-fast 迭代器等设计精巧地平衡了内存、性能与安全,而 subListArrays.asList 因共享底层数组暗藏陷阱。

  • 存储与构造 :底层 Object[] elementData + int size 确定逻辑边界;无参构造延迟分配,首次 add 才扩容至 10,节省空列表内存
  • 扩容oldCapacity + oldCapacity >> 1 确定 1.5 倍新容量,Arrays.copyOf 复制,上限 Integer.MAX_VALUE - 8;可手动 ensureCapacity 减少扩容次数
  • 线程安全elementData[size++] 非原子,并发写会丢数据或抛异常;替代方案有 CopyOnWriteArrayList(读无锁写复制)、Vector(全同步)、Collections.synchronizedList(包装器)
  • fail-fast :迭代器持有 expectedModCount 快照,遍历中任何结构性修改(除迭代器自身的 remove)都会立即抛出 ConcurrentModificationException
  • 序列化elementDatatransient,自定义 writeObject/readObject 只序列化实际元素,反序列化重建恰好容量的数组,省空间且隐藏内部实现
  • subList 陷阱 :返回内部 SubList 视图,与原列表共享数组,任一方结构性修改会导致另一方的迭代器快速失败
  • Arrays.asList :返回固定大小的 Arrays 内部列表,不支持增删,且与原数组联动修改
  • 性能 :连续内存缓存局部性好,遍历远快于 LinkedList;频繁头部插入/删除是灾难,扩容临时数组会增加 GC 压力
  • 实践要点 :预先指定初始容量、用迭代器或 removeIf 安全删除、避免频繁头部操作、警惕 subListArrays.asList 的约束

以下架构图展示了本文的六大篇章及内部模块顺序,你可以据此按需阅读或快速定位感兴趣的章节。

graph TD Start["ArrayList 深度解析"] --> P1 Start --> P2 Start --> P3 Start --> P4 Start --> P5 Start --> P6 subgraph P1["Part 1: 基础认知篇"] direction TB M1["模块1: 定义 核心特性与适用场景"] M2["模块2: 接口与继承体系"] M1 --> M2 end subgraph P2["Part 2: 存储与构造篇"] direction TB M3["模块3: 构造方法与延迟分配"] M4["模块4: 存储结构与核心字段"] M3 --> M4 end subgraph P3["Part 3: 核心原理篇"] direction TB M5["模块5: 扩容机制深度剖析"] M6["模块6: 插入 add"] M7["模块7: 删除 remove"] M8["模块8: 查询 get 与修改 set"] M9["模块9: 其他重要方法"] M5 --> M6 --> M7 --> M8 --> M9 end subgraph P4["Part 4: 迭代与序列化篇"] direction TB M10["模块10: 迭代器与 fail-fast"] M11["模块11: 序列化与 transient"] M10 --> M11 end subgraph P5["Part 5: 进阶与并发篇"] direction TB M12["模块12: subList 与 Arrays.asList 陷阱"] M13["模块13: 并发问题与线程安全方案"] M14["模块14: 内存与 GC 分析"] M12 --> M13 --> M14 end subgraph P6["Part 6: 总结与面试篇"] direction TB M15["模块15: 注意事项与最佳实践"] M16["模块16: 面试高频专题"] M17["模块17: 性能总结与选型建议"] M15 --> M16 --> M17 end
  • Part 1 基础认知篇:从定义、六大特性、适用场景与完整继承关系入手,为后续源码分析打下概念基础。
  • Part 2 存储与构造篇 :重点剖析三种构造器的延迟分配思想,以及 elementDatasizemodCount 三大核心字段的职责与设计意图。
  • Part 3 核心原理篇 :全文技术重心,独立讲解 1.5 倍扩容的算法与均摊分析,随后对增删改查进行源码级拆解,并覆盖 sortcleartrimToSize 等辅助方法。
  • Part 4 迭代与序列化篇 :说明 Itr/ListItr 如何借助 expectedModCount 实现 fail-fast,以及自定义序列化如何绕过 transient 实现省空间、解耦的序列化协议。
  • Part 5 进阶与并发篇 :剖析 subList 视图与 Arrays.asList 的内部实现及陷阱,对比三种线程安全方案,并从内存布局和 GC 角度对性能做出解释。
  • Part 6 总结与面试篇:提炼最佳实践与正反例代码,集中攻克 10 道高频面试题,最后以时间复杂度表与选型建议收尾。

Part 1:基础认知篇

模块 1:定义、核心特性与适用场景

定义
java.util.ArrayList 是基于动态数组实现的 List 接口的可变大小有序集合。它维护一个 Object[] 内部数组,当元素数量超过数组长度时会自动进行容量扩展,从而在逻辑上提供"无限容量"的列表容器。

核心特性提炼

  1. 有序性 :元素按照 add 调用的顺序存储,迭代顺序与插入顺序一致。
  2. 可重复性 :允许元素重复,通过 equals() 判定,同一对象可出现多次。
  3. 随机访问 :实现了 RandomAccess 标记接口,get(int index) 直接以数组下标寻址,时间复杂度 O(1)。
  4. 动态扩容 :自动扩容,采用 oldCapacity + (oldCapacity >> 1) 增长 1.5 倍,兼顾空间与拷贝开销。
  5. 非线程安全:所有 public 方法未加同步,多线程并发写需外部锁或选用线程安全替代品。
  6. 允许 null 元素 :底层 Object[] 可以存放 null,且可多个 null。

适用场景与反例场景

  • ✅ 需要频繁通过索引随机访问、修改元素,或大部分插入发生在尾部。
  • ✅ 遍历操作占主导且期望利用 CPU 缓存行优势的场景。
  • ❌ 频繁在列表头部或中部进行插入、删除,导致大规模数组复制。
  • ❌ 多线程并发读写且未施加同步措施,将产生数据错乱甚至越界异常。

底层实现本质一句话
Object[] 数组 + 动态扩容 + System.arraycopy 元素迁移。

flowchart TD A["需要 List 存储"] --> B{"需要快速随机访问?"} B -->|"否"| C["考虑 LinkedList"] B -->|"是"| D{"需要线程安全?"} D -->|"是"| E{"写多读少?"} D -->|"否"| F["ArrayList"] E -->|"是"| G["CopyOnWriteArrayList"] E -->|"否"| H["Vector / 同步包装器"] F --> I["适用于尾部追加 遍历 索引访问"] G --> J["适用于读远多于写的并发场景"] H --> K["全局同步 适用于低并发或遗留系统"]

图表说明 :根据对随机访问的需求将链表与数组实现分开。如果需要快速随机访问,再检查线程安全要求。线程安全场景下,若写操作频率很低(如配置信息),CopyOnWriteArrayList 是最佳选择,因为写时复制成本高但读完全无锁;若并发度不高或仍需写操作较频繁,可选择 VectorCollections.synchronizedList。如果不需要线程安全,ArrayList 是默认选择。该决策树帮助开发者在不同需求下做出合适的 List 实现选型。


模块 2:接口与继承体系

ArrayList 继承自 AbstractList,从而获得 List 接口的部分骨架实现,进而实现 RandomAccessCloneableSerializable 接口。

classDiagram class Iterable { <> +iterator() Iterator } class Collection { <> +size() int +isEmpty() boolean +contains(Object) boolean +add(E) boolean +remove(Object) boolean } class List { <> +get(int) E +set(int, E) E +add(int, E) void +remove(int) E +indexOf(Object) int +listIterator() ListIterator } class RandomAccess { <> } class Cloneable { <> } class Serializable { <> } class AbstractCollection { <> +isEmpty() boolean +contains(Object) boolean +toArray() Object[] } class AbstractList { <> +modCount int +indexOf(Object) int +lastIndexOf(Object) int +listIterator() ListIterator +subList(int, int) List } class ArrayList { -Object[] elementData -int size +ArrayList() +ArrayList(int) +ArrayList(Collection) +trimToSize() +ensureCapacity(int) } Iterable <|-- Collection Collection <|-- List List <|.. AbstractList RandomAccess <|.. ArrayList Cloneable <|.. ArrayList Serializable <|.. ArrayList Collection <|.. AbstractCollection AbstractCollection <|-- AbstractList AbstractList <|-- ArrayList

图表说明

  • Collection 接口继承自 Iterable,使得所有集合均可使用 for-each 循环。
  • List 接口在 Collection 基础上增加了按索引操作的方法,如 get(int)add(int, E)listIterator() 等。
  • AbstractCollection 提供了 isEmpty()contains()toArray() 等通用实现,AbstractList 在此基础上进一步提供了 indexOflastIndexOflistIterator 以及 subList 的骨架。
  • ArrayList 继承 AbstractList 后只需集中实现数组存储相关的核心操作,如 addgetremove、扩容等。
  • RandomAccess 是标记接口,表明此 List 支持快速随机访问,算法可根据 list instanceof RandomAccess 决定使用索引遍历还是迭代器遍历。
  • CloneableSerializable 使得 ArrayList 支持浅克隆与序列化。

Part 2:存储与构造篇

模块 3:构造方法与延迟分配(源码剖析)

ArrayList 提供了三个构造器,其核心设计思想是延迟分配底层数组------在未添加元素时,尽量复用内部已有的空数组常量,避免堆内存浪费。

3.1 无参构造器 ArrayList()

java 复制代码
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

该构造器不分配任何数组,直接将 elementData 指向常量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这是一个静态的、长度为 0 的 Object[]。它与另一个空数组常量 EMPTY_ELEMENTDATA 的区别在于用途:此常量用于标记"因无参构造而产生的空列表",从而在首次添加元素时触发特殊的扩容策略,将容量直接扩展至 DEFAULT_CAPACITY = 10

3.2 指定初始容量构造器 ArrayList(int initialCapacity)

java 复制代码
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 时,使用 EMPTY_ELEMENTDATA 共享常量。注意:使用 EMPTY_ELEMENTDATA 的列表在后续扩容时不会跳到 10,而是严格按 1.5 倍计算(0 的 1.5 倍还是 0,但 grow 会以 minCapacity 为准)。

3.3 集合构造器 ArrayList(Collection<? extends E> c)

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

先将集合转为数组并赋值给 elementData,再更新 size。但某些集合的 toArray() 返回的数组运行时类型并非 Object[](如子类型数组),这会导致后续可直接存放任意对象的能力消失,因此需要通过 Arrays.copyOf 进行一次防御性拷贝,确保底层数组类型为 Object[]。如果传入集合为空,则使用 EMPTY_ELEMENTDATA,避免维护多余数组。

flowchart TD A["开始构造 ArrayList"] --> B{"参数类型"} B -->|"无参"| C["elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA"] B -->|"int initialCapacity"| D{"initialCapacity > 0?"} D -->|"是"| E["elementData = new Object[initialCapacity]"] D -->|"== 0"| F["elementData = EMPTY_ELEMENTDATA"] D -->|"< 0"| G["抛出 IllegalArgumentException"] B -->|"Collection"| H["c.toArray()"] H --> I{"数组长度是否为 0?"} I -->|"是"| J["elementData = EMPTY_ELEMENTDATA"] I -->|"否"| K{"数组运行时类型是否为 Object[]?"} K -->|"是"| L["直接使用该数组"] K -->|"否"| M["Arrays.copyOf 转为 Object[]"] C --> N["首次 add 时扩容至 DEFAULT_CAPACITY=10"] F --> O["扩容按 1.5 倍正常增长"] J --> O L --> P["size = 数组长度"] M --> P

图表说明

  • 无参构造没有开辟新数组,直接复用常量 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这一空数组引用指向堆中唯一实例,节省内存。
  • 指定初始容量 0 的构造使用另一个常量 EMPTY_ELEMENTDATA,虽然同样长度为 0,但在 ensureCapacityInternal 中不会触发跳至 10 的逻辑。
  • 集合构造器首先要解决 toArray 返回子类型数组的兼容问题,通过类型检查与 Arrays.copyOf 保证了底层数组为 Object[],允许此后加入任意类型的对象。
  • 三种构造器的不同路径最终都将影响首次添加时的扩容行为:无参构造会保证容量至少为 10;其他则仅保证至少容纳 1 个元素,并严格按照 1.5 倍步长增长。

模块 4:存储结构与核心字段(源码剖析)

java 复制代码
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

    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;                 // 元素数量
    protected transient int modCount; // 结构修改次数
}
  • elementData:被 transient 标记,默认序列化将忽略该字段。这是因为数组长度通常大于实际元素数目,默认序列化会浪费大量空间,同时暴露内部实现细节。ArrayList 通过自定义 writeObjectreadObject 实现更紧凑的序列化。
  • size:标记已存入的元素数量,与 elementData.length 无关。
  • modCount:继承自 AbstractList,任意导致数组结构变化(增、删、扩容)的操作均会使该计数器自增,迭代器依赖它与自身快照 expectedModCount 进行一致性校验,实现 fail-fast 机制。

空数组常量对比

  • EMPTY_ELEMENTDATA:用于显式指定容量为 0 的构造或空集合构造,扩容时不会激增至 10。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:专门用于无参构造,首次扩容时保证容量至少 10,优化默认使用场景。
classDiagram class ArrayList { - transient Object[] elementData - int size + int modCount + get(int index) E + add(E e) boolean + remove(int index) E } note for ArrayList "elementData 指向实际数组\nsize 表示有效元素个数\nmodCount 记录修改次数"

图表说明 :该简化类图突出了 ArrayList 的三大核心字段。elementData 是整个容器的物理存储,其长度代表当前容量。size 逻辑边界决定了迭代和访问的范围。modCount 是一个安全度量,凡是可能破坏迭代器一致性的写操作都会使其变化,任何对列表的并发修改探测都依赖于此字段。


Part 3:核心原理篇

模块 5:扩容机制深度剖析(独立模块,核心重点)

扩容机制是 ArrayList 实现"动态"的根本,也是理解其性能特征的关键。

5.1 扩容触发时机

  • add(E e)add(int index, E element)addAll(Collection c) 等插入操作在写入前调用 ensureCapacityInternal
  • readObject 中反序列化完成元素读取后,若数组长度大于 size,可能会进行缩容或调整。
  • 手动调用 ensureCapacity(int minCapacity) 可预先扩容,避免批量添加时的多次数组拷贝。

5.2 源码调用链路

ensureCapacityInternal(size + 1)ensureExplicitCapacity(minCapacity)grow(minCapacity)hugeCapacity(minCapacity)

java 复制代码
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);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    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);
}

5.3 新容量计算:1.5 倍的数学权衡

新容量 newCapacity = oldCapacity + (oldCapacity >> 1),即 1.5 倍。选取 1.5 倍而不是 2 倍,是时间与空间博弈的结果:

  • 2 倍扩容意味着每次扩展后预留的未使用空间可能过多,造成堆内存长期占用过高。
  • 1.5 倍能使得数组大小序列更快地收敛,相邻的释放数组空间更容易被内存分配器复用,减少内存碎片。
    均摊分析下,1.5 倍扩容依然保证插入 n 个元素的拷贝总成本为 O(n),均摊每次插入为 O(1)。

5.4 数组复制成本

Arrays.copyOf 内部调用 System.arraycopy,这是一个 native 方法,实现整块内存拷贝,但其时间复杂度为 O(n)。扩容时的复制会产生一个临时旧数组,成为 GC 回收对象。

5.5 极限容量与 MAX_ARRAY_SIZE

MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。Java 数组长度由 int 表示,理论上最大可为 Integer.MAX_VALUE,但某些 JVM 实现会在数组对象头中存储额外信息,留出 8 字节空间可降低 OOM 风险。若所需容量超过 MAX_ARRAY_SIZEhugeCapacity 方法会尝试返回 Integer.MAX_VALUE(但很可能抛出 OOM)。

5.6 手动扩容 ensureCapacity

该方法允许用户提前将数组扩容至指定最小容量,避免在大量 add 时多次触发 incremental 扩容及多次数组拷贝。

5.7 与 C++ vector 对比

C++ 标准库中 std::vector 通常使用 2 倍增长。相较之下,Java 1.5 倍在内存效率上更优,但可能导致稍多的扩容次数。Java 的选择偏向节省内存,符合托管运行时对 GC 压力敏感的设计哲学。

flowchart TD A["外部调用 add 等方法"] --> B["ensureCapacityInternal(size+1)"] B --> C{"elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA?"} C -->|"是"| D["minCapacity = max(10, size+1)"] C -->|"否"| E["minCapacity = size+1"] D --> F["ensureExplicitCapacity(minCapacity)"] E --> F F --> G{"minCapacity > elementData.length?"} G -->|"否"| H["直接返回 无需扩容"] G -->|"是"| I["modCount++ 记录结构修改"] I --> J["调用 grow(minCapacity)"] J --> K["计算 oldCapacity = elementData.length"] K --> L["newCapacity = oldCapacity + oldCapacity>>1"] L --> M{"newCapacity < minCapacity?"} M -->|"是"| N["newCapacity = minCapacity"] M -->|"否"| O{"newCapacity > MAX_ARRAY_SIZE?"} N --> O O -->|"是"| P["hugeCapacity 处理 尝试 Integer.MAX_VALUE"] O -->|"否"| Q["Arrays.copyOf 拷贝元素到新数组"] P --> Q Q --> R["elementData 指向新数组 旧数组待GC"]

图表说明

  1. 任何添加操作首先调用 ensureCapacityInternal,将当前 size+1 作为最小容量需求传递。
  2. elementData 为无参构造专属空数组,则最小容量至少为 10,这一步实现了默认容量 10 的语义。
  3. ensureExplicitCapacity 判断当前数组长度是否小于最小需求,若小于则扩容;任何时候进入该方法都会递增 modCount,因为扩容也属于结构性修改。
  4. grow 首先记录旧容量,然后移位相加得到 1.5 倍新容量。若该新容量仍不满足要求(例如一次性 addAll 大量元素),则以 minCapacity 为准。
  5. 新容量与 MAX_ARRAY_SIZE 比较,超限则调用 hugeCapacity 处理,可能抛出 OutOfMemoryError。
  6. 最终通过 Arrays.copyOf 创建新数组、复制旧元素,并将引用赋给 elementData,旧数组将失去引用而被垃圾回收。

模块 6:核心操作------插入 add(结合源码)

6.1 尾部追加 add(E e)

java 复制代码
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

检查容量后直接在 size 位置赋值并自增。在多线程环境下,elementData[size++] 分解为读、存、写三步,非原子操作,可能导致数据覆盖。

6.2 指定位置插入 add(int index, E element)

java 复制代码
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

先检查索引合法性(0 ≤ index ≤ size),然后扩容确认,接着将 index 开始的所有元素整体后移一位,再插入新元素。移动的元素数量为 size - index,平均复杂度 O(n)。

6.3 addAll(Collection<? extends E> c)

实现类似,先转换为数组,一次扩容到位,然后通过两次 System.arraycopy(或一次合并)将集合元素插入尾部或指定位置。

Demo 代码

java 复制代码
List<String> list = new ArrayList<>();
list.add("A");                // 尾部追加
list.add("B");
list.add(1, "C");            // 在索引1处插入,B 后移
System.out.println(list);    // [A, C, B]

List<String> another = Arrays.asList("X", "Y");
list.addAll(0, another);     // 头部批量插入
System.out.println(list);    // [X, Y, A, C, B]
flowchart TD subgraph "尾部追加" T1["add(E e)"] --> T2["ensureCapacityInternal(size+1)"] T2 --> T3["elementData[size] = e"] T3 --> T4["size++"] end subgraph "中间插入" M1["add(int index, E e)"] --> M2["rangeCheckForAdd(index)"] M2 --> M3["ensureCapacityInternal(size+1)"] M3 --> M4["System.arraycopy 后移 size-index 个元素"] M4 --> M5["elementData[index] = e"] M5 --> M6["size++"] end

图表说明 :尾部追加代码路径极短,只涉及容量检查、赋值与 size 自增,性能最佳。中间插入则额外调用 rangeCheckForAdd 检查 index 是否在 [0, size] 区间内,随后必须通过 System.arraycopy 移动内存块;移动规模与插入位置成反比,越靠近头部成本越高,极端情况下可能导致 O(n) 级的复制开销,这也是不推荐在 ArrayList 头部频繁插入的原因。


模块 7:核心操作------删除 remove(结合源码)

7.1 按索引删除 remove(int index)

java 复制代码
public E remove(int index) {
    rangeCheck(index);
    modCount++;
    E oldValue = elementData(index);
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null;
    return oldValue;
}

rangeCheck 验证 index 小于 size,随后自增 modCount,计算需要前移的元素个数,若大于 0 则执行数组复制将后续元素覆盖上来,最后把原本最后一个有效位置(现 size-1)置为 null 以断开引用,让 GC 可以回收。

7.2 按对象删除 remove(Object o)

java 复制代码
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;
}

区分 null 与非 null 情况,线性遍历查找第一个匹配元素,然后调用 fastRemove(跳过索引范围检查的私有方法)执行实际的删除和移动。时间复杂度 O(n)。

Demo 代码

java 复制代码
List<String> list = new ArrayList<>(Arrays.asList("A","B","C","D"));
list.remove(2);        // 删除索引 2 的 "C"
list.remove("A");      // 删除对象 "A"
System.out.println(list); // [B, D]
flowchart TD A["remove(int index)"] --> B["rangeCheck(index)"] B --> C["modCount++"] C --> D["numMoved = size - index - 1"] D --> E{"numMoved > 0?"} E -->|"是"| F["System.arraycopy 将 index+1..size-1 前移"] E -->|"否"| G["elementData[--size] = null"] F --> G G --> H["返回旧值"]

图表说明 :删除操作的第一步进行索引上限检查,合法的 index 必须小于 size。modCount 自增标志着列表结构变化,可能导致并发迭代器立即失败。接着计算需要移动的元素数,若待删除元素不是最后一个,则调用 native 方法 System.arraycopy 完成数组块平移,最后将抛弃的尾部引用置为 null,帮助 GC 回收不再需要的对象。这一细节防止了因 ArrayList 长期持有对象引用而导致的内存泄漏。


模块 8:核心操作------查询 get 与修改 set

8.1 get(int index)

java 复制代码
public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}
E elementData(int index) {
    return (E) elementData[index];
}

8.2 set(int index, E element)

java 复制代码
public E set(int index, E element) {
    rangeCheck(index);
    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

两者均先调用 rangeCheck 确认索引在 [0, size) 范围内,然后直接通过数组下标访问目标槽位。因为数组在内存中连续,可在 O(1) 时间内完成。

Demo 代码

java 复制代码
List<Integer> list = new ArrayList<>(Arrays.asList(10, 20, 30));
int val = list.get(1);      // 20
list.set(1, 99);            // 将索引1处修改为99
System.out.println(list);   // [10, 99, 30]
flowchart TD A["get(int index)"] --> B["rangeCheck(index)"] B --> C["return elementData[index]"] D["set(int index, E e)"] --> E["rangeCheck(index)"] E --> F["old = elementData[index]"] F --> G["elementData[index] = e"] G --> H["return old"]

图表说明 :这两个操作极其简单,全程只有索引检查和数组寻址,没有任何循环或复制。这正是 RandomAccess 接口所承诺的快速随机访问能力。在列表遍历时,若已知集合为 ArrayList,应优先使用 for 索引循环而非迭代器,从而充分利用此 O(1) 特性。


模块 9:其他重要方法(源码简析)

  • clear() :循环将 elementData[0..size-1] 置 null,size 归零,modCount++,数组容量保持不变。
  • contains(Object o) / indexOf / lastIndexOf :顺序扫描数组并调用 equals 比较,定位第一个或最后一个匹配元素。
  • trimToSize() :如果 size < elementData.length,则用 Arrays.copyOf 创建一个长度精确等于 size 的新数组并将元素拷贝进去,用于释放预留空间。通常用在列表不再增长的情况下。
  • sort(Comparator<? super E> c) :直接委托 Arrays.sort(elementData, 0, size, c),排序算法为 TimSort(针对部分有序数据的优化归并)。排序执行完毕后,modCount++ 以维持 fail-fast 语义。
  • forEach(Consumer<? super E> action) :内部使用 final int expectedModCount = modCount 构建迭代器,对每个元素调用 action.accept,期间若发现并发修改立即抛出异常。
sequenceDiagram participant caller participant ArrayList participant Arrays participant TimSort caller->>ArrayList: sort(comparator) ArrayList->>Arrays: sort(elementData, 0, size, comparator) Arrays->>TimSort: sort(a, lo, hi, c) Note over TimSort: 基于归并和二分插入的稳定排序 TimSort-->>Arrays: 排序完成 Arrays-->>ArrayList: ArrayList->>ArrayList: modCount++

图表说明sort 方法的调用链展示了 ArrayList 如何将排序能力委托给 java.util.Arrays。当数据量超过阈值(默认 32)时,Arrays.sort 会启用 TimSort 算法,该算法先找出数组中已经有序的片段(run),然后利用归并思想合并这些片段,对较小 run 使用二分插入排序。排序过程完全发生在 elementData 内部,仅在必要时产生临时数组。排序完成后 modCount 自增,保证任何正在迭代中的迭代器立刻检测到结构变化并抛出 ConcurrentModificationException


Part 4:迭代与序列化篇

模块 10:迭代器深度剖析------fail-fast 的完整实现

ArrayList 的迭代器由内部类 Itr 实现 Iterator 接口,ListItr 继承 Itr 并实现 ListIterator 接口,支持双向遍历与元素替换。

10.1 Itr 核心字段

  • int cursor:下一次 next() 要返回的元素的索引,初始为 0。
  • int lastRet = -1:最近一次通过 next()previous() 返回的元素索引,-1 表示没有。
  • int expectedModCount = modCount:创建迭代器时记录的修改计数快照。

10.2 next()checkForComodification

java 复制代码
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

每次调用 next 先验证 modCount 的一致性,然后检查游标是否越界,最后返回当前元素并前移 cursor 与设置 lastRet。

10.3 remove() 的协同工作

java 复制代码
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

必须先调用 next()(lastRet ≥ 0)才可删除。删除先委托外部类 remove(lastRet),随后将 cursor 调整为 lastRet,使得下次 next() 返回原本的下一个元素;lastRet 重置为 -1 防止重复删除。最关键的一步:更新 expectedModCount = modCount,同步外部修改计数,这样迭代器在删除后依然有效。

10.4 ListItr 的增强

ListItr 扩展了 Itr,提供 previous()set(E e)add(E e) 等方法。add 方法通过调用外层 ArrayList.this.add(cursor, e) 并同步 expectedModCount,而 set 直接赋值并检测 lastRet

flowchart TD subgraph "next流程" N1["next()"] --> N2["checkForComodification"] N2 --> N3{"cursor < size?"} N3 -->|"否"| N4["NoSuchElementException"] N3 -->|"是"| N5["获取 elementData[cursor] cursor自增"] N5 --> N6["lastRet = 旧cursor 返回元素"] end subgraph "remove流程" R1["remove()"] --> R2{"lastRet >= 0?"} R2 -->|"否"| R3["IllegalStateException"] R2 -->|"是"| R4["checkForComodification"] R4 --> R5["ArrayList.this.remove(lastRet)"] R5 --> R6["cursor = lastRet"] R6 --> R7["lastRet = -1"] R7 --> R8["expectedModCount = modCount"] end

图表说明

  • next 流程中,第一步的 checkForComodification 确保自迭代器创建或上次同步后,外部列表没有发生结构性变化。随后判断游标是否已经达到 size,防止越界。取元素后,cursor 指向下一个待返回索引,lastRet 记录本次返回索引,为 remove 操作提供依据。
  • remove 流程中,首先检查 lastRet 合法性,确保之前调用过 next 且未重复 remove。然后再次检查并发修改,通过外层 ArrayList.remove 删除元素;删除会触发数组移动,并更新 modCount。迭代器通过将 cursor 回退至 lastRet,并重置 lastRet 为 -1,最后将 expectedModCount 刷新为最新的 modCount,保证后续遍历不会误判并发修改。这就是迭代器对"结构性修改"的安全窗口机制。

模块 11:序列化与 transient 的设计智慧(源码解读)

elementData 声明为 transient,若依赖默认序列化将丢失全部元素数据。ArrayList 通过自定义 writeObjectreadObject 实现了更优的序列化方案。

11.1 writeObject(java.io.ObjectOutputStream s)

java 复制代码
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
    int expectedModCount = modCount;
    s.defaultWriteObject();          // 写入非 transient 字段(size等)
    s.writeInt(elementData.length);  // 写入当前容量,便于反序列化预分配
    for (int i=0; i<size; i++) {
        s.writeObject(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}

先调用 defaultWriteObject 保存 size 等非 transient 数据,然后将数组 length 写入流用于预分配,再循环仅序列化 size 个有效元素,跳过空槽位。尾部再次校验并发修改。

11.2 readObject(java.io.ObjectInputStream s)

java 复制代码
private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {
    elementData = EMPTY_ELEMENTDATA;
    s.defaultReadObject();           // 读取 size 等
    int capacity = s.readInt();
    if (capacity > 0) {
        ensureCapacityInternal(size); // 扩容至至少容纳 size 个元素
        Object[] a = elementData;
        for (int i=0; i<size; i++) {
            a[i] = s.readObject();
        }
    }
}

先重置数组为空,读取 size,然后根据之前写出的容量申请相应的内部数组,并逐一读入元素放入数组。这样可以保证反序列化后的 elementData 长度恰为 size(或稍大但无浪费),避免序列化垃圾数据。

sequenceDiagram participant List as ArrayList participant OOS as ObjectOutputStream participant OIS as ObjectInputStream Note over List: 序列化开始 List->>OOS: writeObject(this) OOS->>List: defaultWriteObject() 写入 size, modCount OOS->>List: writeInt(elementData.length) 写入容量 loop 遍历 size 次 OOS->>List: writeObject(elementData[i]) 仅写有效元素 end OOS-->>List: 完成 Note over List: 反序列化开始 List->>OIS: readObject() OIS->>List: defaultReadObject() 读取 size OIS->>List: readInt() 读取预分配容量 List->>List: ensureCapacityInternal(size) 扩容 loop size 次 OIS->>List: readObject() 填充元素 end

图表说明

  • 序列化阶段,ArrayList 首先把元数据(size、modCount 拷贝)通过默认协议写出,然后特意写出当前数组容量以便反序列化预分配;关键步骤是循环写出实际元素,而尾部的 null 空槽全部忽略,大大减小序列化产物体积。
  • 反序列化时,通过先读 size,再根据容量字段一次性分配足够大的数组,然后逐个读回元素。这种方法使得即使原 ArrayList 预留了大量空闲容量,序列化流中也仅包含实际数据。反序列化后的容量可能比原实例小,但元素完整保留。
  • 整个过程中,任何并发的结构修改都会导致 modCount 变化并被捕获,抛出异常保证序列化数据一致性。

Part 5:进阶与并发篇

模块 12:subList 视图陷阱与 Arrays.asList 差异(源码剖析)

12.1 subList 的内部实现

ArrayListsubList(int fromIndex, int toIndex) 返回一个内部类 SubList 实例,该实例不复制数据,而是直接持有外部 ArrayList 的引用,并通过 offsetsize 映射视图范围。任何对 SubList 的非结构性操作(如 get、set)都会被转换为对外部列表对应索引的操作;结构性修改(如 add、remove)也会反映到外部列表。

关键源码结构

java 复制代码
private class SubList extends AbstractList<E> implements RandomAccess {
    private final AbstractList<E> parent;
    private final int parentOffset;
    private int size;
    ...
}

由于 SubList 和原列表共享同一个 elementData,JDK 文档明确要求:在通过 SubList 修改列表后,不能再直接修改原列表的结构,否则后续对 SubList 的操作将引发 ConcurrentModificationException 。这是因为 SubList 内部记录 modCount 快照,任何一方的结构性修改都会使另一方的迭代器或方法检查失败。

12.2 Arrays.asList 的陷阱

Arrays.asList(T... a) 返回的是 Arrays 的一个私有内部类 ArrayList,它并非 java.util.ArrayList。该内部类也基于原始数组,但不支持结构性修改 (即 add、remove 会直接抛出 UnsupportedOperationException)。同时,由于它直接引用传入的数组,对列表的 set 操作会影响原数组,反之亦然。

Demo 代码:两条陷阱重现

java 复制代码
// 陷阱1:subList 与原列表互相干扰
List<String> list = new ArrayList<>(Arrays.asList("A","B","C","D"));
List<String> sub = list.subList(1, 3);
list.add("E"); // 结构性修改原列表
// sub.get(0);  // 可能抛出 ConcurrentModificationException
// sub.add("X"); // 也会异常

// 陷阱2:Arrays.asList 固定大小
List<Integer> fixedList = Arrays.asList(1, 2, 3);
fixedList.set(0, 99);   // OK
// fixedList.add(4);     // 抛出 UnsupportedOperationException

安全使用原则

  • 若要通过 subList 操作原列表,期间不要直接对原列表进行结构性修改。
  • 使用 new ArrayList<>(Arrays.asList(...)) 将固定列表转换为真正的可变 ArrayList。
  • 理解 subList 只是一个视图,需要独立列表时应当新建 ArrayList 拷贝。

模块 13:并发问题与线程安全方案对比(结合源码与并发分析)

13.1 非线程安全根源

  • add(E e) 中的 elementData[size++] = e 不是原子操作:先读取 size,然后存值,再写回 size+1。多线程可能导致覆盖或数组越界。
  • 扩容期间,线程 A 触发 grow,重新分配数组并拷贝数据,此时线程 B 可能仍在旧数组上操作,造成数据丢失或 NPE。
  • modCount 的更新没有同步,迭代器可能在并发修改时得到不确定行为而不是可靠的 fail-fast。

以下是线程不安全的简易演示:

java 复制代码
List<String> list = new ArrayList<>();
Runnable task = () -> {
    for (int i = 0; i < 1000; i++) list.add("x");
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(list.size()); // 结果往往小于 2000,甚至抛出异常

13.2 线程安全方案对比

  1. Vector

    所有 public 方法均使用 synchronized 关键字修饰,锁粒度是整个方法,性能较差。

  2. Collections.synchronizedList

    返回一个包装器 SynchronizedRandomAccessList,内部使用 mutex 对象同步,每个方法调用都会加锁。迭代时仍需开发者手动同步。

  3. CopyOnWriteArrayList

    写入时加 ReentrantLock,创建底层数组的新副本,在新副本上修改,然后通过 volatile 将数组引用指向新数组。读操作无锁,直接访问当前数组,适合读多写少的场景。

sequenceDiagram participant T1 as 线程1 (写) participant T2 as 线程2 (读/写) participant SyncList as synchronizedList participant COWA as CopyOnWriteArrayList Note over T1,SyncList: 互斥锁包装 T1->>SyncList: add(e) SyncList-->>SyncList: 获取 mutex 锁 SyncList->>SyncList: delegate.add(e) SyncList-->>T1: 释放锁 T2->>SyncList: get(i) SyncList-->>SyncList: 获取 mutex 锁 SyncList->>SyncList: delegate.get(i) SyncList-->>T2: 释放锁 Note over T1,COWA: 写时复制 T1->>COWA: add(e) COWA-->>COWA: 获取 ReentrantLock COWA->>COWA: 复制原数组并扩容 COWA->>COWA: 将新元素放入新数组并更新引用 COWA-->>T1: 释放锁 T2->>COWA: get(i) COWA->>COWA: 直接返回 elements[i] (无锁) COWA-->>T2: 返回

图表说明

  • synchronizedList 在每次调用时都会竞争同一把锁,读操作也被阻塞,并发度低。
  • CopyOnWriteArrayList 写操作加锁并复制整个数组,内存开销大但读操作完全无锁且极其快速;写操作完成后数组引用通过 volatile 更新,读线程即刻可见。该方案在迭代过程中不抛 ConcurrentModificationException,因为迭代器持有的是快照数组引用。
  • 性能权衡:写多场景下 CopyOnWriteArrayList 拷贝开销巨大,不适合;低并发整体同步可用 Vector 或同步包装器;高并发读多写少选 CopyOnWriteArrayList;大量写操作应考虑并发队列或显式锁分段。

模块 14:内存占用与 GC 影响分析

对象内存布局

一个 ArrayList 实例本身占用对象头(Mark Word + Class Pointer),加上 elementData 引用、sizemodCount 等字段,典型 64 位 JVM 开启压缩指针下约 24 字节。存储的主体是 elementData 数组,其内存为连续分配的数组对象头 + 容量 * 引用大小。

扩容带来的临时数组

每次扩容都会创建新数组并将旧数组内容复制过去,旧数组失去引用后成为垃圾,导致 GC 压力。若频繁尾部追加并触发多次扩容,会生成多个不同大小的临时数组,有可能触发 Full GC。预先指定足够容量可显著缓解。

与 LinkedList 的内存对比

LinkedList 每个节点为一个独立 Node 对象(对象头 + 前驱后继指针 + 数据引用),占用内存分散,内存碎片较多,且遍历时缓存不友好。ArrayList 连续内存布局使得迭代时 CPU 缓存行命中率高,大多数元素可在同一缓存行内取得,性能优势明显。

缩容操作
trimToSize() 可以将预留空间释放,但会产生一次数组复制和旧数组回收,建议在列表不再变化时使用。


Part 6:总结与面试篇

模块 15:注意事项与最佳实践

  1. 基本类型需装箱

    ArrayList 不能直接存储 intlong 等基本类型,必须使用包装类,自动装箱会带来性能与内存开销。
    正例List<Integer> list = new ArrayList<>(); list.add(42);
    反例 :试图利用 new ArrayList<int>() 编译错误。

  2. foreach 删除陷阱

    使用增强 for 循环内部调用 list.remove(obj) 会触发 ConcurrentModificationException,因为迭代器内部 modCount 被意外修改。
    错误示例

    java 复制代码
    for (String s : list) { if (s.equals("del")) list.remove(s); } // 异常

    正确示例 :使用迭代器的 remove()removeIf

    java 复制代码
    Iterator<String> it = list.iterator();
    while (it.hasNext()) { if (it.next().equals("del")) it.remove(); }
    // 或 Java 8+
    list.removeIf(s -> s.equals("del"));
  3. 频繁头部插入性能灾难

    每次头部插入会导致整个数组后移,O(n) 开销。
    反例list.add(0, e) 在循环内导致平方级复杂度。
    正确 :改用 LinkedListArrayDeque,或最后反转列表。

  4. 预估容量

    若事先能估算元素数量,使用 new ArrayList<>(expectedSize) 避免多次扩容。
    示例 :处理 10000 条数据,直接 new ArrayList<>(10000) 可减少约十余次数组复制。

  5. subList 不可与原列表同时结构性修改

    如模块 12 所述,获得 subList 后,原列表的增减操作会使 subList 失效抛出异常。

  6. 返回不可变视图

    公共 API 返回列表时,若不想被外部修改,使用 Collections.unmodifiableList(list) 包装。

  7. null 元素的存在与处理

    ArrayList 允许 null,但 sort 时若 Comparator 未处理 null 可能导致 NPE;indexOf(null) 可正确找到 null 位置。


模块 16:面试高频专题(独立且极尽详细)

以下 10 个面试必考问题,覆盖所有核心知识点。

1. ArrayList 的默认容量与扩容机制?
标准回答 :无参构造创建的 ArrayList 在首次添加元素时,容量会从 0 直接扩展为 10;后续扩容采用 1.5 倍递推(oldCapacity + (oldCapacity >> 1))。若一次性添加大量元素,新容量可能变为 minCapacity。最大容量受限于 Integer.MAX_VALUE - 8
追问 1 :为什么是 1.5 倍而不是 2 倍?

回答:2 倍扩容会导致内存迅速膨胀,未使用的预留空间浪费严重;1.5 倍较好地平衡了拷贝次数和空间浪费,且增长序列在内存分配器中更易复用。
追问 2MAX_ARRAY_SIZE 为何减 8?

回答:部分 JVM 在数组对象头中保留了 8 字节额外信息,直接申请 Integer.MAX_VALUE 容易导致 OOM,减 8 作为安全余量。
加分回答 :结合均摊分析,证明 n 次插入的复制次数上界为 2n,故均摊 O(1)。另外可提及 ensureCapacity 手动扩容的优化。

2. elementData 为何用 transient,序列化如何保证元素不丢失?
标准回答elementData 长度通常大于 size,默认序列化会存储大量空引用导致空间浪费并暴露内部数组大小。ArrayList 通过自定义 writeObjectreadObject 仅序列化 size 个实际元素,并在反序列化时重建合适容量的数组。
追问 1writeObject 中为什么还要写 elementData.length?

回答:用于反序列化时预分配正确的容量,避免多次 incremental 扩容。
追问 2 :序列化过程中出现并发修改会怎样?

回答:tail 部分的 modCount 检查将抛出 ConcurrentModificationException,保证序列化快照一致性。
加分回答:这种外部方法实现序列化是 Serializable 接口的一种扩展机制,ObjectOutputStream 通过反射调用 private 方法,是框架设计的经典案例。

3. ArrayList 与 LinkedList 的性能全面对比?
标准回答 :ArrayList 随机访问 O(1),尾部追加均摊 O(1),插入/删除需移动元素 O(n);LinkedList 随机访问 O(n),头尾插入删除 O(1),节点插入删除仅需修改引用,但遍历时内存跳跃,缓存不友好。
追问 1 :缓存行对两者遍历性能的实际影响?

回答:ArrayList 连续内存可充分利用 CPU 缓存行预取,遍历速度远快于指针跳转的 LinkedList。
追问 2 :内存占用差异?

回答:ArrayList 仅需一个数组及预留容量;LinkedList 每个节点有两个指针开销,内存占用远大于 ArrayList,特别是在存储小对象时。
加分回答:可以举实际 JMH 基准测试数字,说明遍历 ArrayList 通常比 LinkedList 快 3~5 倍。

4. subList 陷阱及内部 SubList 源码机制?
标准回答 :subList 返回内部类 SubList,持有原列表引用,修改视图会直接影响原列表,反之亦然。结构性修改将导致双方 modCount 不一致,继续访问抛出 ConcurrentModificationException
追问 1 :为什么 SubList 没有独立复制数据?

回答:节省内存并保持视图实时性,这是典型的代理/视图设计模式。
追问 2 :如何获得一个独立的子列表?

回答:new ArrayList<>(list.subList(from, to))
加分回答:可提及 SubList 自身的 modCount 检查是如何基于父列表实现的,以及为什么它的 add、remove 方法都会调用父列表内部方法。

5. Arrays.asList 返回的 List 与 new ArrayList 的区别?
标准回答Arrays.asList 返回的是 java.util.Arrays 的私有内部类 ArrayList,固定大小、不支持增删,且与原数组共享存储,对 set 操作会反映在原数组上。new ArrayList 则是真正可变、自动扩容的独立列表。
追问 1 :为什么 Arrays.asList 不能 add 或 remove?

回答:其内部没有实现 add/remove 方法,继承自 AbstractList 的这两个方法默认抛 UnsupportedOperationException
追问 2 :如何将数组安全转为可变 List?

回答:new ArrayList<>(Arrays.asList(array))
加分回答 :解释自动装箱问题------Arrays.asList(1,2,3) 会被推断为 List<int[]> 而非 List<Integer>,因为泛型不支持基本类型,可能产生意想不到的结果。

6. modCount 如何实现 fail-fast,遍历时安全删除方式?
标准回答 :迭代器创建时 snapshot expectedModCount,任何结构性修改使 modCount 自增,下一次 next/remove 检测到不等即抛异常。安全删除须用迭代器自身的 removeremoveIf
追问 1modCount 为什么被标记为 protected transient?

回答:protected 允许子类复用 fail-fast 机制;transient 避免序列化无意义的状态值。
追问 2 :如果在迭代时使用迭代器的 add(ListIterator)会怎样?

回答:ListIterator.add 会同步 expectedModCount,因此是安全的。
加分回答 :可谈论 modCount 溢出问题------理论上 int 溢出后取负值可能导致检查失效,但在有生之年不太可能发生。

7. ArrayList 线程不安全的原因及多种安全方案对比?
标准回答elementData[size++] 非原子、扩容竞争、modCount 无同步等导致并发问题。方案包括 Vector(全方法 synchronized)、Collections.synchronizedList(同步包装器)、CopyOnWriteArrayList(写时复制,读无锁)。
追问 1 :同步包装器迭代时为什么需要手动加锁?

回答:包装器提供的 iterator() 返回的迭代器自身不是线程安全的,文档要求用户必须在迭代时同步在包装器对象上。
追问 2 :CopyOnWriteArrayList 适合什么场景?为什么不适合频繁写?

回答:适合读多写少,如缓存配置;写时复制整个数组开销巨大,频繁写会产生大量复制和 GC 压力。
加分回答:可从内存屏障、volatile 语义和锁升级角度深入对比。

8. remove 为何要移动元素并末尾置 null?
标准回答 :移动元素是为了保持数组连续性,保证索引 O(1) 访问;末尾置 null 是为了断开引用,让 GC 能够回收被删对象,防止内存泄漏。
追问 1 :如果不置 null 会怎样?

回答:若列表长期持有该引用,即使业务上已删除,对象仍可达,造成逻辑上的内存泄漏。
追问 2clear() 方法里也是类似逻辑吗?

回答:clear() 会遍历所有位置置 null,但容量维持不变。
加分回答:可以联系 Effective Java 中"消除过期的对象引用"条目。

9. System.arraycopy 和 Arrays.copyOf 在源码中的使用区别?
标准回答System.arraycopy 是 native 方法,需要自行管理目标数组的创建;Arrays.copyOf 内部调用 System.arraycopy,但自动创建新数组并返回,常用于扩容。在 ArrayList 中,growArrays.copyOf,而 add(index)remove 使用 System.arraycopy 在同一个数组内部移位。
追问 1 :为什么不统一使用一个?

回答:Arrays.copyOf 每次新建数组,适用于扩容;内部移位用 arraycopy 避免产生新数组对象。
追问 2System.arraycopy 支持重叠复制吗?

回答:是的,其实现保证了源与目标区域重叠时的正确性。
加分回答 :提及 Arrays.copyOf 的类型安全转换特性,如 copyOf(U[], int, Class<? extends T[]>)

10. 构造器为什么要用延迟分配(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)?
标准回答 :避免空 ArrayList 实例立刻占用至少 10 个引用大小的内存。大量空列表在延迟分配下共享同一个空数组常量,有效节省堆空间。首次添加才分配,实现懒初始化。
追问 1 :为什么使用两个不同的空数组常量?

回答:区分无参构造和显式 0 容量,使无参构造能实现首次扩容至 10 的约定,而后者严格按 1.5 倍扩容。
追问 2 :这种设计会有性能代价吗?

回答:每次添加都需要检查是否 == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,但只是一次引用比较,开销极小。
加分回答:可以提到这种设计在 Java 7/8 后引入,早期版本无参构造直接分配 10 长度数组,改进后显著减少内存占用。


模块 17:性能总结与选型建议

核心操作时间复杂度

操作 时间复杂度 备注
add(E) 尾部 均摊 O(1) 扩容时 O(n) 但均摊 O(1)
add(int, E) O(n) 需要移动 n/2 个元素平均
get(int) O(1) 纯数组寻址
set(int, E) O(1) 同上
remove(int) O(n) 移动元素
remove(Object) O(n) 查找 O(n) + 移动 O(n)
contains / indexOf O(n) 线性扫描
iteration O(n) 顺序访问,缓存友好
sort O(n log n) TimSort,可能附带临时数组

缓存友好性

ArrayList 的连续内存布局使得每次迭代都能充分利用 CPU 缓存行,相比链表结构在大数据量遍历场景优势显著。

选型建议

  • 必须使用 ArrayList:需要频繁索引随机访问、尾部追加为主、一次性遍历处理,且能预估容量避免频繁扩容。
  • 果断放弃 ArrayList :频繁头部或中部插入删除成为性能瓶颈,或需要双向队列操作时应使用 LinkedListArrayDeque;高并发读写环境需换成 CopyOnWriteArrayList 或并发队列。
  • 内存敏感场景 :列表不再增长后调用 trimToSize() 释放预留空间,或用 new ArrayList<>(0) 避免默认 10 的首次扩容。
相关推荐
BING_Algorithm2 小时前
JDBC核心教程
java·后端·mysql
京师20万禁军教头2 小时前
33面向对象(中级)-object类详解
java
一个小浪吴啊2 小时前
重构 AI 编程流:基于 Hermes 记忆中枢与 OpenCode 执行终端的 Harness 工程化实践
java·人工智能·opencode·harness·hermes
Lyyaoo.2 小时前
【JAVA网络面经】应用层协议
java·开发语言·网络
無限進步D2 小时前
Java 面向对象高级 继承
java·开发语言
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【37】ReactAgent 构建、执行流程分析
java·人工智能·spring
是宇写的啊2 小时前
MyBaties
java·开发语言·mybatis
钝挫力PROGRAMER2 小时前
程序中事件机制的实现
java·后端·python·软件工程
程序员威哥2 小时前
Java调用YOLO模型性能优化实战:CPU/GPU加速与内存优化全指南
java·人工智能·后端