适用读者:被
ConcurrentModificationException和subList坑过的人。
前置:S04-B08、S03-B02(数组 vs 链表)。
实测环境:OpenJDK 21.0.12.1(2026-11-05)。
覆盖旧文:A-1113(383) A-1112(292) A-1671(76) A-1664(117) A-1675(104) A-1667(93) = 1,065 点。
覆盖扩展:E-S04-011(fail-fast 与 modCount)。
上一篇:S04-X01 · 八股速查与跨系列索引
下一篇:S04-J02 · HashMap 源码。
1. 一句话结论
ArrayList 的"快"来自连续内存 + 摊还 O(1) 的尾部追加;迭代器的"严格"来自 modCount 校验——它宁可抛异常,也不让你在遍历中偷偷改动。
实测三条:
1. foreach 删除 → ConcurrentModificationException
2. Iterator.remove() → [1, 2, 4, 5]
3. subList.clear() 后原列表 → [1, 4, 5]
2. ArrayList 的三个关键事实
| 事实 | 说明 |
|---|---|
底层是 Object[] |
连续内存,随机访问 O(1) |
| 扩容 1.5 倍 | newCap = oldCap + (oldCap >> 1) |
| 尾部追加摊还 O(1) | 扩容那次是 O(n),均摊到 n 次是 O(1) |
"默认容量 10"是个常见误解:new ArrayList<>() 初始是空数组,第一次 add 才分配 10。
| 操作 | 复杂度 | 备注 |
|---|---|---|
get(i) |
O(1) | 下标直算 |
add(e) |
摊还 O(1) | 可能扩容 |
add(0, e) |
O(n) | 全体右移(S03-B02 实测慢 23.7×) |
remove(i) |
O(n) | 左移 |
contains |
O(n) | 线性扫描 |
3. 迭代器与 fail-fast(E-S04-011)
实测 1:for (Integer i : list) { if (i == 3) list.remove(i); } → CME。
原因:ArrayList.Itr 在每次 next() 时检查 modCount != expectedModCount,而 list.remove 会改 modCount。
| 做法 | 结果 |
|---|---|
foreach + list.remove |
❌ CME |
Iterator.remove() |
✅ 安全(它同步更新 expectedModCount) |
list.removeIf(...) |
✅ 最简洁(Java 8+) |
| 多线程修改 | ❌ 不是 fail-fast 能解决的(要并发容器,S05) |
fail-fast 不是"安全保证":它是尽力检测,不保证一定抛。不要用它做正确性依赖。
4. subList:一个"视图",不是副本(实测 3)
List<Integer> l3 = new ArrayList<>(Arrays.asList(1,2,3,4,5));
List<Integer> sub = l3.subList(1,3);
sub.clear();
// 实测:l3 → [1, 4, 5]
| 事实 | 后果 |
|---|---|
subList 返回视图(内部持有原列表引用 + offset) |
改视图 = 改原列表 |
| 原列表结构改动后,视图操作会抛 CME | 不能"拿着视图不放" |
| 要独立副本 | new ArrayList<>(list.subList(a,b)) |
5. LinkedList 与 ArrayDeque:什么时候用哪个
| 结构 | 头插/尾插 | 随机访问 | 内存 |
|---|---|---|---|
ArrayList |
尾 O(1),头 O(n) | O(1) | 紧凑 |
LinkedList |
O(1) | O(n) | 每节点 2 指针 + 对象头 |
ArrayDeque |
两端 O(1) | 不支持 | 环形数组,紧凑 |
实测结论(S03-B02/B03):ArrayDeque 做栈/队列比 LinkedList 快 2.8×。LinkedList 只在"频繁中间插入且已持有节点"或 LRU 场景有用。
6. 五个误区
| 误区 | 真相 |
|---|---|
"ArrayList 默认容量 10" |
空数组,首次 add 才分配 |
"foreach 里能删元素" |
抛 CME(除 removeIf/Iterator) |
| "fail-fast 保证线程安全" | 只是尽力检测 |
"subList 是副本" |
是视图 |
"LinkedList 更适合做队列" |
ArrayDeque 更快更省 |
7. 自检
ArrayList扩容倍数是多少?为什么尾插是摊还 O(1)?- CME 由什么机制触发?哪两种写法可以安全删除?
subList.clear()会影响原列表吗?为什么?- 为什么
fail-fast不能当"线程安全"用? - 栈/队列该选
LinkedList还是ArrayDeque?
8. 下一篇
S04-J02 · HashMap 源码:哈希、扩容、树化与线程不安全
下一篇用实测的 table 长度变化(16 → 32 → 64)讲清扩容时机,并解释"容量为什么必须是 2 的幂"。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-1113/1112/1671/1664/1675/1667;扩展点 E-S04-011。三条行为为 OpenJDK 21 实测。