ArrayList
ArrayList 是 Java 中最常用的 List 实现,底层基于动态数组。它适合保存有序、可重复、以读取和尾部追加为主的数据。
很多业务代码中的默认列表都应该先考虑 ArrayList,因为它 API 简洁、访问速度快、内存连续性好、CPU 缓存友好。
# 1. 核心特点
| 特点 | 说明 |
|---|---|
| 有序 | 按插入顺序保存元素 |
| 可重复 | 允许相同元素出现多次 |
允许 null | 可以存放多个 null |
| 动态扩容 | 容量不足时创建更大数组并复制 |
| 随机访问快 | get(index) 是 O(1) |
| 头部和中间插入删除较慢 | 需要移动元素 |
| 非线程安全 | 多线程写入需要额外同步 |
示例:
List<String> names = new ArrayList<>();
names.add("Tom");
names.add("Jerry");
names.add("Tom");
System.out.println(names.get(1)); // Jerry
# 2. 底层结构
ArrayList 内部维护一个对象数组和元素个数。
ArrayList
┌────────────────────┐
│ elementData ───┐ │
│ size = 3 │ │
└────────────────┼───┘
▼
Object[]
┌──────┬───────┬──────┬──────┬──────┐
│ Tom │ Jerry │ Bob │ null │ null │
└──────┴───────┴──────┴──────┴──────┘
index 0 1 2 3 4
注意区分:
| 概念 | 含义 |
|---|---|
size | 当前元素个数 |
capacity | 底层数组容量 |
List<String> list = new ArrayList<>(100);
System.out.println(list.size()); // 0
指定初始容量是预留数组空间,不是放入了 100 个元素。
# 3. 构造方式
常见构造方法:
new ArrayList<>();
new ArrayList<>(initialCapacity);
new ArrayList<>(collection);
使用建议:
| 构造方式 | 适合场景 |
|---|---|
new ArrayList<>() | 不知道大概元素数量 |
new ArrayList<>(1000) | 能预估元素数量,减少扩容 |
new ArrayList<>(source) | 基于已有集合创建可变副本 |
如果从不可变集合创建可变列表:
List<String> source = List.of("A", "B");
List<String> mutable = new ArrayList<>(source);
mutable.add("C");
# 4. 添加元素
尾部添加:
list.add("A");
流程:
检查容量
│
├─ 容量足够 -> elementData[size] = e
│
└─ 容量不足 -> 扩容 -> 复制旧数组 -> 写入新元素
尾部添加平均是 O(1),但遇到扩容时会退化为 O(n),因为需要复制旧数组。
指定位置添加:
list.add(1, "X");
结构变化:
插入前:
[A, B, C, _, _]
在 index=1 插入 X:
[A, X, B, C, _]
└── B、C 右移
中间插入需要移动元素,时间复杂度 O(n)。
# 5. 获取和修改元素
随机访问:
String value = list.get(2);
过程:
elementData[index]
数组支持通过下标直接定位,所以是 O(1)。
修改:
list.set(2, "NewValue");
set 不改变集合结构,通常不会触发 fail-fast。
# 6. 删除元素
按索引删除:
String old = list.remove(1);
结构变化:
删除前:
[A, B, C, D, _]
remove(1):
[A, C, D, _, _]
└── C、D 左移
删除后,尾部多出来的位置会被置为 null,帮助 GC 回收不再使用的对象。
按对象删除:
list.remove("A");
它会从头遍历,删除第一个匹配元素。
注意重载陷阱:
List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3));
numbers.remove(1); // 删除 index=1 的元素,结果 [1, 3]
numbers.remove(Integer.valueOf(1)); // 删除元素 1
# 7. 扩容机制
当元素数量超过底层数组容量时,ArrayList 会扩容。常见 JDK 实现中,新容量大约是旧容量的 1.5 倍。
扩容过程:
旧数组容量不足
│
▼
计算新容量
│
▼
创建新数组
│
▼
复制旧元素
│
▼
elementData 指向新数组
示意:
old elementData
[A, B, C]
│ 扩容复制
▼
new elementData
[A, B, C, _, _]
扩容成本来自数组复制。大量添加元素时,如果能预估数量,应提前指定初始容量:
List<Order> orders = new ArrayList<>(expectedSize);
或者调用:
((ArrayList<Order>) orders).ensureCapacity(expectedSize);
前提是变量真实类型就是 ArrayList。
# 8. trimToSize
trimToSize() 可以把底层数组容量缩小到当前 size。
ArrayList<String> list = new ArrayList<>(10000);
list.add("A");
list.trimToSize();
适合场景:
- 大批量构建完成后,列表长期保留。
- 需要减少多余容量占用。
不适合频繁调用,因为它也会复制数组。
# 9. 遍历方式
常见遍历:
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
for (String value : list) {
System.out.println(value);
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
遍历时删除:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String value = iterator.next();
if (value.startsWith("A")) {
iterator.remove();
}
}
不要在增强 for 中直接 list.remove(value)。
# 10. subList 的坑
subList 返回的是原列表的视图,不是独立副本。
List<String> list = new ArrayList<>(List.of("A", "B", "C"));
List<String> sub = list.subList(0, 2);
sub.set(0, "X");
System.out.println(list); // [X, B, C]
如果需要独立副本:
List<String> copy = new ArrayList<>(list.subList(0, 2));
原列表结构性修改后,再访问 subList 可能抛出 ConcurrentModificationException。
# 11. toArray 的正确用法
推荐:
String[] array = list.toArray(new String[0]);
或者如果已知大小:
String[] array = list.toArray(new String[list.size()]);
不要直接强转:
String[] array = (String[]) list.toArray(); // ClassCastException
因为无参 toArray() 返回的是 Object[]。
# 12. 线程安全
ArrayList 不是线程安全的。多线程同时写可能导致:
- 数据覆盖。
size不准确。- 读取到未完整写入的数据。
- 扩容期间状态不一致。
选择:
| 场景 | 推荐 |
|---|---|
| 单线程或方法内局部变量 | ArrayList |
| 多线程读多写少 | CopyOnWriteArrayList |
| 简单同步包装 | Collections.synchronizedList |
| 高并发生产消费 | 阻塞队列 |
同步包装遍历时需要手动同步:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());
synchronized (syncList) {
for (String value : syncList) {
System.out.println(value);
}
}
# 13. ArrayList 与 LinkedList
| 对比项 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组 | 双向链表 |
| 随机访问 | O(1) | O(n) |
| 尾部添加 | 均摊 O(1) | O(1) |
| 头部添加 | O(n) | O(1) |
| 中间插入 | O(n) 移动元素 | O(n) 定位节点 |
| 内存占用 | 较低 | 每个节点有前后指针 |
| CPU 缓存友好 | 好 | 较差 |
实际项目中,除非明确需要双端队列语义,否则 ArrayList 通常比 LinkedList 更适合作为普通列表。
# Tips 快问快答
Q:ArrayList 默认容量一定是 10 吗?
A:默认构造时通常先是空数组,第一次添加元素时才扩到默认容量。不要把“构造后立即有 10 个容量”理解得过死。
Q:size 和容量有什么区别?
A:size 是元素个数,容量是底层数组长度。
Q:ArrayList 查询为什么快?
A:底层数组可以通过下标直接定位元素。
Q:为什么中间删除慢? A:删除后面的元素需要整体左移。
Q:remove(1) 删除的是元素 1 还是下标 1?
A:对于 List<Integer>,remove(1) 优先匹配按下标删除。
Q:subList 是新集合吗?
A:不是独立副本,而是原列表视图。
Q:ArrayList 可以存 null 吗?
A:可以,而且可以存多个 null。
Q:ArrayList 线程安全吗?
A:不安全,多线程写入应使用并发集合或同步控制。
Q:大量 add 怎么优化? A:预估数量并指定初始容量,减少扩容复制。
Q:为什么普通列表默认选 ArrayList?
A:它读写常见场景性能均衡,内存和缓存表现也通常优于链表。