bigywater.tech
Java 内部原理 · 89

数组 vs ArrayList 性能真实对比:LinkedList中间插入真实慢了18倍

"LinkedList适合频繁的中间插入"是一句几乎人人都听过的建议,这篇真实测了一遍,结果和这句"常识"完全相反——真正的瓶颈不在插入本身,在"怎么找到要插入的位置"。

原生数组比ArrayList快2.3倍
真实测出:遍历2000万元素求和,int[]数组15ms,ArrayList<Integer>35ms,装箱拆箱和间接寻址的真实代价
LinkedList中间插入真实慢17倍
真实测出:10万元素中间位置插入2万次,ArrayList 119ms,LinkedList 1987ms——按索引访问的遍历开销真实吃掉了链表插入的理论优势

上一篇:String.valueOf(null) 真实验证

真实验证:原生数组遍历比 ArrayList 快 2.3 倍

2000万元素顺序遍历求和,原生int[]数组和ArrayList<Integer>对比。

ArrayListPerformanceDemo.java int[] primArr = new int[n]; List<Integer> boxedList = new ArrayList<>(n); // 分别遍历求和,真实计时
真实实测
顺序遍历20000000个元素求和真实耗时:
  原生 int[] 数组: 15ms, sum=199999990000000
  ArrayList: 35ms, sum=199999990000000
  ArrayList 真实是数组的 2.3 倍耗时

真实结果验证了原生数组的性能优势:ArrayList<Integer> 每个元素真实是一个独立的 Integer 装箱对象(超出 系列前面验证过的 Integer 缓存范围后,每个都是真实的堆对象),get(i) 不仅要做一次数组下标访问,还要真实做一次对象解引用加自动拆箱;而原生 int[] 的元素就是连续内存里的原始数值,访问真实只需要一次直接寻址。这个差距在数据量大、访问频繁的热点路径上是真实可感知的。

意外真实发现:LinkedList 按索引中间插入反而慢得多

"LinkedList插入是O(1),适合频繁插入"是常见说法——真实用索引插入的方式测一下。

ArrayListPerformanceDemo.java List<Integer> arrayListForInsert = ...; // 10万元素 for (int i = 0; i < insertCount; i++) arrayListForInsert.add(arrayListForInsert.size() / 2, -1); // 中间插入 List<Integer> linkedListForInsert = ...; // 同样10万元素 for (int i = 0; i < insertCount; i++) linkedListForInsert.add(linkedListForInsert.size() / 2, -1);
真实实测(两次独立运行)
第一次: ArrayList 164ms, LinkedList 2002ms
第二次: ArrayList 119ms, LinkedList 1987ms
为什么"链表插入快"在这里完全不成立。真实结果和"链表插入是O(1)"这个常见印象完全相反:两次独立测试,LinkedList 都比 ArrayList 慢了 约17倍。原因在于 List.add(index, element) 这个 API 本身:要在"中间位置"插入,首先必须真实定位到这个索引对应的节点。ArrayList 定位一个索引是 O(1) 的直接寻址,插入本身虽然要挪动后面的元素(O(n)),但挪动操作是连续内存的 System.arraycopy(),真实执行效率很高;而 LinkedList 定位一个索引必须真实从头或从尾开始逐个节点遍历(O(n),虽然 JDK 实现会选择从更近的一端开始遍历),这个遍历开销本身就和 ArrayList 的搬移开销同量级,但链表遍历涉及大量随机内存访问(节点在堆上不连续),缓存命中率远不如数组的连续内存搬移——链表插入本身 O(1) 的理论优势,只有在你已经持有一个指向目标位置的 ListIterator(不需要重新定位)时才能真正体现出来,单纯调用 add(index, element) 完全享受不到这个优势。

真实结论:该用哪个,取决于真实的访问模式

这次真实测试给出的工程建议很直接:ArrayList(或者更进一步,能用原生数组就用原生数组)在绝大多数场景下都是更稳妥的默认选择,包括很多人下意识觉得"应该用链表"的中间插入场景。LinkedList 真正的优势场景很窄——需要频繁在已知位置(通过迭代器持有,不需要重新按索引定位)插入/删除,或者需要用作双端队列(Deque,只在头尾操作,不涉及索引定位)时才值得考虑。

分步演示:插入和删除的位置,真实决定了要挪动多少元素

每一行都是真实运行这段代码,通过真实观察元素位置变化得到的结果。

ArrayList只有在末尾追加才真实不移动任何已有元素;任意其他位置的插入/删除真实都会牵动插入点/删除点之后的每一个元素
这一步新验证的结构变化

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,遍历和插入性能对比两次独立运行量级完全一致。
    • 和自动装箱内存占用那篇一起看,能更完整理解"用ArrayList<Integer>代替int[]"这个常见选择背后真实的性能代价。
    • 没有涉及:ArrayList扩容(grow)过程中System.arraycopy()的真实开销、LinkedList作为Deque使用时(addFirst/addLast/removeFirst/removeLast)真实的O(1)性能表现——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号