263. Java 集合 - 遍历 List 时选用哪种方式?ArrayList vs LinkedList

35 阅读3分钟

263. Java 集合 - 遍历 List 时选用哪种方式?ArrayList vs LinkedList


🔁 两种常见遍历方式

我们有两种遍历方式:

🟦 方式一:使用索引 for (int i = 0; i < size; i++)
for (int index = 0; index < ints.size(); index++) {
   var v = ints.get(index); // 读取第 index 个元素
}

✅ 适用于支持随机访问的集合,如 ArrayList

🟩 方式二:使用增强 for(隐式 Iterator
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 再遍历?🤔

试试看这个例子:

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)说明
15.182一次遍历,复制成本占 4%
1014.031很快 amortize 掉复制成本
100100.104相比原始遍历快了很多

📌 建议:

如果你要遍历多次、或者做随机读取,复制 LinkedList 是划算的!


🧠 性能背后的本质:Pointer Chasing & Cache Miss

💡 为什么 LinkedList 慢?

因为每个元素都在堆内存的不同位置,遍历时:

  • 需要不断“追踪”下一节点地址(pointer chasing);
  • 容易触发 CPU 的 cache miss,大幅增加访问延迟。

🧠 类比讲解建议:

就像你家书架有编号,但每本书放在不同房间,还要问人借钥匙开门 —— 慢!

📌 对比数组结构:

特性ArrayListLinkedList
元素位置连续内存离散内存(靠引用串联)
读取成本固定偏移 → 快(O(1)多次跳转 → 慢(O(n)
遍历友好性✅ 高❌ 差

🧾 最终建议总结表格

操作场景建议使用容器建议遍历方式
随机读取、频繁访问元素ArrayList✅ index / for-each
插入/删除频繁(头/尾)LinkedList✅ iterator`
遍历一次都可接受LinkedList 别用 index
遍历多次ArrayList 或先复制for-each
大规模遍历性能敏感ArrayListfor-each