文章目录
- [263. Java 集合 - 遍历 List 时选用哪种方式?`ArrayList` vs `LinkedList`](#263. Java 集合 - 遍历 List 时选用哪种方式?
ArrayListvsLinkedList) -
-
- [🔁 两种常见遍历方式](#🔁 两种常见遍历方式)
-
- [🟦 方式一:使用索引 `for (int i = 0; i < size; i++)`](#🟦 方式一:使用索引
for (int i = 0; i < size; i++)) - [🟩 方式二:使用增强 `for`(隐式 `Iterator`)](#🟩 方式二:使用增强
for(隐式Iterator))
- [🟦 方式一:使用索引 `for (int i = 0; i < size; i++)`](#🟦 方式一:使用索引
- [🔍 对于 `ArrayList`,两种方式几乎一样](#🔍 对于
ArrayList,两种方式几乎一样) -
- [⏱ 性能对比(1000 元素)](#⏱ 性能对比(1000 元素))
- [⚠️ 对于 `LinkedList`,差别巨大!](#⚠️ 对于
LinkedList,差别巨大!) -
- [⏱ 性能对比(`1000` 元素)](#⏱ 性能对比(
1000元素))
- [⏱ 性能对比(`1000` 元素)](#⏱ 性能对比(
- [🔄 是否应该复制 `LinkedList` 再遍历?](#🔄 是否应该复制
LinkedList再遍历?) - [⏱ 基准测试:复制后再迭代](#⏱ 基准测试:复制后再迭代)
- [🧠 性能背后的本质:`Pointer Chasing & Cache Miss`](#🧠 性能背后的本质:
Pointer Chasing & Cache Miss) -
- [💡 为什么 `LinkedList` 慢?](#💡 为什么
LinkedList慢?)
- [💡 为什么 `LinkedList` 慢?](#💡 为什么
- [🧾 最终建议总结表格](#🧾 最终建议总结表格)
-
263. Java 集合 - 遍历 List 时选用哪种方式?ArrayList vs LinkedList
🔁 两种常见遍历方式
我们有两种遍历方式:
🟦 方式一:使用索引 for (int i = 0; i < size; i++)
java
for (int index = 0; index < ints.size(); index++) {
var v = ints.get(index); // 读取第 index 个元素
}
✅ 适用于支持随机访问的集合,如
ArrayList。
🟩 方式二:使用增强 for(隐式 Iterator)
java
for (var v : ints) {
// v 是集合中的每个元素
}
✅ 实际上等价于使用
Iterator,但JVM编译器可对其优化。
🔍 对于 ArrayList,两种方式几乎一样
因为 ArrayList 底层是数组,所以:
ints.get(i)实际是数组索引读取(O(1))Iterator本质上也是一层封装,差别极小
⏱ 性能对比(1000 元素)
| 遍历方式 | 耗时 (ms/op) |
|---|---|
Iterator (for-each) |
1.447 |
Index 访问 |
1.986 |
📌 小结:
- 虽然
index方式管理变量稍重,但差异不大; JIT编译器可以省略Iterator对象的创建 ,提高for-each效率。
⚠️ 对于 LinkedList,差别巨大!
LinkedList.get(index) 每次都从头或尾走一遍链表,因此:
list.get(i)是O(n)操作- 遍历中调用
get(i)n 次 → 总复杂度O(n²)😱
⏱ 性能对比(1000 元素)
| 遍历方式 | 耗时 (ms/op) |
|---|---|
Iterator (for-each) |
4.950 |
Index 访问 |
584.889 |
💥 足足慢了 100 倍!
🧠 类比讲解建议:
就像走迷宫,每走一步都得重新从入口出发 ------ 耗时自然爆炸。
📌 总结建议:
- ❌ 千万不要用
get(i)遍历LinkedList - ✅ 使用
Iterator才是正解
🔄 是否应该复制 LinkedList 再遍历?
既然 LinkedList 指针太慢,那有没有可能......先复制成 ArrayList 再遍历?🤔
试试看这个例子:
java
var ints = IntStream.range(0, 1000)
.boxed()
.collect(Collectors.toCollection(LinkedList::new));
var copy = ints.stream().toList(); // 复制成 List(底层数组)
for (var v : copy) {
// 访问元素
}
📌 为什么这招有效?
toList()会直接创建一个刚好大小的数组,效率高;- 创建过程耗时低,只有迭代一次的 4%;
- 如果需要遍历多次,复制成本很快就能摊平。
⏱ 基准测试:复制后再迭代
| 遍历次数 | 耗时 (ms/op) | 说明 |
|---|---|---|
| 1 | 5.182 | 一次遍历,复制成本占 4% |
| 10 | 14.031 | 很快 amortize 掉复制成本 |
| 100 | 100.104 | 相比原始遍历快了很多 |
📌 建议:
如果你要遍历多次、或者做随机读取,复制
LinkedList是划算的!
🧠 性能背后的本质:Pointer Chasing & Cache Miss
💡 为什么 LinkedList 慢?
因为每个元素都在堆内存的不同位置,遍历时:
- 需要不断"追踪"下一节点地址(
pointer chasing); - 容易触发 CPU 的
cache miss,大幅增加访问延迟。
🧠 类比讲解建议:
就像你家书架有编号,但每本书放在不同房间,还要问人借钥匙开门 ------ 慢!
📌 对比数组结构:
| 特性 | ArrayList |
LinkedList |
|---|---|---|
| 元素位置 | 连续内存 | 离散内存(靠引用串联) |
| 读取成本 | 固定偏移 → 快(O(1)) |
多次跳转 → 慢(O(n)) |
| 遍历友好性 | ✅ 高 | ❌ 差 |
🧾 最终建议总结表格
| 操作场景 | 建议使用容器 | 建议遍历方式 |
|---|---|---|
| 随机读取、频繁访问元素 | ✅ ArrayList |
✅ index / for-each |
| 插入/删除频繁(头/尾) | ✅ LinkedList |
✅ iterator` |
| 遍历一次 | 都可接受 | ❗ LinkedList 别用 index |
| 遍历多次 | ✅ ArrayList 或先复制 |
✅ for-each |
| 大规模遍历性能敏感 | ✅ ArrayList |
✅ for-each |