Skip to content

ArrayList 全过程原理

ArrayList 是 Java 最常用的 List 实现。它看起来只是“动态数组”,但真正写生产代码时,要理解数组为什么查询快、扩容为什么慢、插入删除为什么移动元素、modCount 为什么能 fail-fast、subList 为什么容易踩坑、Arrays.asList 为什么不能 add,以及大批量数据如何预估容量。

学习目标

目标要能说清楚
底层结构elementData 数组和 size 分别表示什么
add 流程容量够直接写,不够扩容复制
get/set 流程为什么按下标访问快,越界怎么判断
remove 流程为什么中间删除要移动元素
扩容原理JDK 8 常见 1.5 倍扩容和数组拷贝
fail-fastmodCount 如何发现并发修改
常见坑subListArrays.asList、边遍历边删除
商业场景批量导入、分页查询、临时结果集怎么用
面试闭环能把原理、复杂度、坑点和优化讲清楚

ArrayList 底层是什么

ArrayList 底层核心是一个 Object 数组:

java
transient Object[] elementData;
private int size;

可以理解为:

mermaid
flowchart TD
    A["ArrayList"] --> B["elementData数组"]
    A --> C["size=3"]
    B --> D["0: A"]
    B --> E["1: B"]
    B --> F["2: C"]
    B --> G["3: null 可用容量"]
字段含义
elementData.length当前数组容量
size实际元素个数
空余位置已分配但还没使用的数组空间

容量和元素个数不是一回事。容量是数组能装多少,size 是当前已经装了多少。

get 为什么快

java
String value = list.get(3);

ArrayList 可以直接通过下标定位数组位置:

mermaid
flowchart TD
    A["get(index)"] --> B{"index 是否越界"}
    B -->|是| C["抛 IndexOutOfBoundsException"]
    B -->|否| D["返回 elementData[index]"]

数组在内存里是连续结构,JVM 可以根据数组起始位置和下标快速定位元素,所以 get 通常是 O(1)。

add 到尾部流程

mermaid
flowchart TD
    A["add(element)"] --> B{"size + 1 是否超过容量"}
    B -->|否| C["elementData[size] = element"]
    C --> D["size++"]
    B -->|是| E["grow 扩容"]
    E --> F["复制旧数组到新数组"]
    F --> C

容量够时,尾部追加非常快。

容量不够时,必须创建更大的新数组并复制旧元素,这就是扩容成本。

扩容为什么是性能点

JDK 8 中常见扩容策略可以理解为:

text
newCapacity = oldCapacity + oldCapacity / 2

也就是约 1.5 倍扩容。

mermaid
flowchart TD
    A["旧数组容量 10"] --> B["容量不够"]
    B --> C["创建新数组容量 15"]
    C --> D["System.arraycopy 复制旧元素"]
    D --> E["elementData 指向新数组"]

为什么不是每次只加 1?

如果每次只加 1,连续 add 10000 次就可能复制 9999 次,成本极高。

为什么不是每次翻 10 倍?

扩太大会浪费大量内存。1.5 倍是时间和空间之间的折中。

指定初始容量

如果预计要放 10 万条数据:

java
List<String> rows = new ArrayList<>(100000);

这样能减少扩容和数组复制。

错误写法:

java
List<String> rows = new ArrayList<>();
for (String row : source) {
    rows.add(row);
}

如果数据量很大,默认容量会多次扩容。批量导入、报表导出、一次性查询结果转换时,应该尽量预估容量。

中间插入为什么慢

java
list.add(1, "X");

需要把 index 后面的元素整体后移:

mermaid
flowchart TD
    A["add(index, element)"] --> B{"index 是否越界"}
    B -->|是| C["抛异常"]
    B -->|否| D["确保容量足够"]
    D --> E["index 后元素整体右移"]
    E --> F["写入新元素"]
    F --> G["size++"]

示例:

text
原数组: [A, B, C, D]
add(1, X)
移动后: [A, _, B, C, D]
写入后: [A, X, B, C, D]

所以 ArrayList 适合尾部追加和按下标读取,不适合大量头部或中间插入。

删除为什么慢

java
list.remove(1);

删除中间元素后,后面的元素要左移:

mermaid
flowchart TD
    A["remove(index)"] --> B{"index 是否越界"}
    B -->|是| C["抛异常"]
    B -->|否| D["保存旧元素"]
    D --> E["index 后元素整体左移"]
    E --> F["最后一个位置置 null"]
    F --> G["size--"]
    G --> H["返回旧元素"]

最后一个位置置 null 很重要,它可以让不再使用的对象尽快被 GC 回收。

遍历时删除怎么写

错误写法:

java
for (String item : list) {
    if (item.startsWith("tmp")) {
        list.remove(item);
    }
}

这可能抛 ConcurrentModificationException。增强 for 背后使用 Iterator,遍历期间直接修改 list 会让 modCount 不一致。

正确写法一:

java
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if (item.startsWith("tmp")) {
        iterator.remove();
    }
}

正确写法二:

java
list.removeIf(item -> item.startsWith("tmp"));

fail-fast 原理

ArrayList 有一个 modCount,表示结构性修改次数。Iterator 创建时会记录 expectedModCount

mermaid
flowchart TD
    A["创建 Iterator"] --> B["记录 expectedModCount"]
    B --> C["遍历 next"]
    C --> D{"expectedModCount 是否等于 modCount"}
    D -->|是| E["继续遍历"]
    D -->|否| F["抛 ConcurrentModificationException"]

结构性修改包括 add、remove、clear 等会改变列表结构的操作。set(index, value) 只是替换元素,不改变 size,通常不是结构性修改。

注意:fail-fast 是尽力而为的错误检测机制,不是线程安全机制。不能依赖它保证并发正确。

subList 的坑

java
List<String> list = new ArrayList<>(List.of("A", "B", "C", "D"));
List<String> sub = list.subList(1, 3);

sub 不是独立新 ArrayList,而是原 list 的视图。

mermaid
flowchart TD
    A["原 ArrayList"] --> B["elementData"]
    C["subList"] --> B
    C --> D["记录 offset 和 size"]

风险:

操作风险
修改 subList会影响原 list
修改原 list 结构subList 后续操作可能抛 ConcurrentModificationException
把 subList 长期缓存可能间接引用原大数组,导致内存无法释放

如果需要独立列表:

java
List<String> copy = new ArrayList<>(list.subList(1, 3));

Arrays.asList 的坑

java
List<String> list = Arrays.asList("A", "B", "C");
list.add("D"); // 抛 UnsupportedOperationException

原因:Arrays.asList 返回的是 java.util.Arrays 的内部固定长度 List,不是真正的 java.util.ArrayList。它底层仍然包着原数组,不能改变长度。

如果需要可增删:

java
List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
list.add("D");

ArrayList 线程安全吗

ArrayList 不是线程安全的。多线程同时 add 可能出现:

问题原因
size 不准size++ 不是原子操作
元素覆盖多线程写同一个下标
扩容异常多线程同时扩容和复制
读到 nullsize 更新和元素写入顺序没有同步保障

多线程场景可选:

场景方案
写少读多CopyOnWriteArrayList
需要外部同步Collections.synchronizedList
高并发队列BlockingQueue
批量结果汇总每个线程本地 List,最后合并

ArrayList 和 LinkedList 怎么选

对比ArrayListLinkedList
底层数组双向链表
随机访问快,O(1)慢,O(n)
尾部追加通常快
中间插入删除定位后还要移动元素定位慢,但节点操作快
内存占用相对低每个节点有前后指针,开销大
实际常用度更常用较少用于普通业务 List

不要简单背“LinkedList 插入删除快”。如果要先按下标找到位置,LinkedList 定位成本很高。普通业务列表优先 ArrayList。

商业场景:批量导入

java
import java.util.ArrayList;
import java.util.List;

public class ImportBuffer {
    public List<String> loadRows(List<String> source) {
        List<String> rows = new ArrayList<>(source.size());
        for (String row : source) {
            if (row != null && !row.isBlank()) {
                rows.add(row);
            }
        }
        return rows;
    }
}

为什么指定 source.size()

源数据量已知,提前分配容量,避免 add 过程中多次扩容复制。

商业场景:分页结果转换

java
import java.util.ArrayList;
import java.util.List;

record UserEntity(Long id, String name) {
}

record UserVO(Long id, String displayName) {
}

public class PageConvert {
    public List<UserVO> convert(List<UserEntity> entities) {
        List<UserVO> result = new ArrayList<>(entities.size());
        for (UserEntity entity : entities) {
            result.add(new UserVO(entity.id(), entity.name()));
        }
        return result;
    }
}

分页转换是 ArrayList 很适合的场景:数据量有限、顺序遍历、尾部追加、最终返回。

常见坑

后果正确做法
大批量 add 不设容量多次扩容复制预估容量
增强 for 中 removeConcurrentModificationException用 Iterator.remove 或 removeIf
误以为 subList 是新 List修改互相影响或异常new ArrayList<>(subList)
Arrays.asList 后 addUnsupportedOperationException包一层 new ArrayList
多线程共享写 ArrayList数据丢失、异常使用线程安全容器或隔离合并
remove 后不理解置 null以为只是 size--置 null 是为了帮助 GC

面试标准回答

ArrayList 底层是什么

ArrayList 底层是 Object 数组,elementData 表示数组容量,size 表示实际元素个数。按下标查询时可以直接访问数组位置,所以随机访问通常是 O(1)。

ArrayList 扩容过程

add 时如果容量够,就写入 elementData[size] 并让 size 加一;如果容量不够,就创建更大的新数组,JDK 8 常见是约 1.5 倍扩容,然后用数组拷贝把旧元素复制过去,再写入新元素。扩容涉及新数组分配和元素复制,所以大批量添加应预估容量。

ArrayList 删除为什么慢

删除中间元素后,后面的元素必须整体左移,最后一个位置还要置 null 方便 GC 回收不再使用的对象。因此 ArrayList 适合随机访问和尾部追加,不适合大量头部或中间插入删除。

fail-fast 是什么

ArrayList 的 Iterator 创建时会记录 expectedModCount,遍历过程中如果发现 expectedModCount 和 List 的 modCount 不一致,就抛 ConcurrentModificationException。它是尽力而为的并发修改检测,不是线程安全机制。

ArrayList 和 LinkedList 怎么选

普通业务优先 ArrayList,因为随机访问快、内存开销小、CPU 缓存友好。LinkedList 虽然节点插入删除本身快,但通常还要先遍历定位节点,而且每个节点有额外指针开销,所以并不适合大多数普通列表场景。

关联知识点

知识点为什么要看
集合框架集合体系总览
数组ArrayList 底层依赖数组和数组拷贝
HashMap全过程对比数组列表和哈希表的不同设计
BlockingQueue全过程多线程队列场景不要用普通 ArrayList
JDK版本差异了解集合在不同 JDK 中的演进