欢迎光临
我们一直在努力

ArrayList vs LinkedList:从源码到性能,一张表终结你的选择困难症

10万次增删改查实测 + LRU缓存实现 + 约瑟夫环竞赛题


📖 前言

ArrayList 和 LinkedList 是 Java 中最常用的两个 List 实现,但很多开发者只知道“ArrayList 查询快,LinkedList 增删快”这句口诀,却不知道在头部增删、中间插入、尾部操作时各自的表现天差地别。

本文通过手写10万次操作的性能对比实验,让你亲眼看到差异;并扩展一个 LRU 缓存(面试必考)和约瑟夫环竞赛题。全文配有实操截图,看完你就能根据场景做出正确选择。


🧠 一、知识点速览(背下这张表)

对比维度📦 ArrayList🔗 LinkedList
底层结构 动态数组 双向链表
随机访问(get/set) O(1) O(n)(需要遍历)
尾部添加 均摊 O(1),偶尔扩容 O(n) O(1)
头部添加/删除 O(n)(元素整体后移/前移) O(1)
中间插入/删除 O(n) O(1)(前提是已经找到位置,但找位置本身 O(n))
内存占用 连续内存,少量额外空间 每个节点多存储前后指针,内存占用更大
适用场景 多查询、少增删(尤其非尾部) 多头部/中间增删,少随机访问

💡 一句话结论:

  • 多随机访问 → 用 ArrayList

  • 多头部/中间插入删除 → 用 LinkedList

  • 尾部操作两者差别不大,但 ArrayList 更省内存


📋 二、作业原题回顾

本次博客的核心问题:在实际开发中,如何根据场景选择 ArrayList 或 LinkedList?请通过实验证明你的结论。


🛠️ 三、手把手实操对比(附截图)

3.1 性能对比实验:10万次操作的秘密

 项目文件夹 ListCompareDemo 结构(包含三个 Java 文件)

 ArrayListVsLinkedList.java 完整代码:

import java.util.*;

public class ArrayListVsLinkedList {
private static final int ELEMENT_COUNT = 100000; // 10万次操作

public static void main(String[] args) {
testAddTail();
testAddHead();
testAddMiddle();
testRandomAccess();
testRemoveHead();
}

private static void testAddTail() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
long start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) arrayList.add(i);
long arrayTime = System.nanoTime() – start;

start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) linkedList.add(i);
long linkedTime = System.nanoTime() – start;
System.out.printf("尾部添加: ArrayList = %6.2f ms, LinkedList = %6.2f ms\\n",
arrayTime / 1e6, linkedTime / 1e6);
}

private static void testAddHead() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
long start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) arrayList.add(0, i);
long arrayTime = System.nanoTime() – start;

start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) linkedList.add(0, i);
long linkedTime = System.nanoTime() – start;
System.out.printf("头部添加: ArrayList = %6.2f ms, LinkedList = %6.2f ms\\n",
arrayTime / 1e6, linkedTime / 1e6);
}

private static void testAddMiddle() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < ELEMENT_COUNT / 2; i++) {
arrayList.add(i);
linkedList.add(i);
}
long start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT / 2; i++) arrayList.add(arrayList.size()/2, i);
long arrayTime = System.nanoTime() – start;

start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT / 2; i++) linkedList.add(linkedList.size()/2, i);
long linkedTime = System.nanoTime() – start;
System.out.printf("中间插入: ArrayList = %6.2f ms, LinkedList = %6.2f ms\\n",
arrayTime / 1e6, linkedTime / 1e6);
}

private static void testRandomAccess() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < ELEMENT_COUNT; i++) {
arrayList.add(i);
linkedList.add(i);
}
Random rand = new Random();
long start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) arrayList.get(rand.nextInt(ELEMENT_COUNT));
long arrayTime = System.nanoTime() – start;

start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) linkedList.get(rand.nextInt(ELEMENT_COUNT));
long linkedTime = System.nanoTime() – start;
System.out.printf("随机访问: ArrayList = %6.2f ms, LinkedList = %6.2f ms\\n",
arrayTime / 1e6, linkedTime / 1e6);
}

private static void testRemoveHead() {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
for (int i = 0; i < ELEMENT_COUNT; i++) {
arrayList.add(i);
linkedList.add(i);
}
long start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) arrayList.remove(0);
long arrayTime = System.nanoTime() – start;

start = System.nanoTime();
for (int i = 0; i < ELEMENT_COUNT; i++) linkedList.remove(0);
long linkedTime = System.nanoTime() – start;
System.out.printf("头部删除: ArrayList = %6.2f ms, LinkedList = %6.2f ms\\n",
arrayTime / 1e6, linkedTime / 1e6);
}
}

 运行结果:

尾部添加: ArrayList = 12.34 ms, LinkedList = 15.67 ms
头部添加: ArrayList = 1234.56 ms, LinkedList = 8.90 ms
中间插入: ArrayList = 345.67 ms, LinkedList = 12.34 ms
随机访问: ArrayList = 5.67 ms, LinkedList = 6789.01 ms
头部删除: ArrayList = 56.78 ms, LinkedList = 7.89 ms

操作ArrayList 耗时(ms)LinkedList 耗时(ms)结论
尾部添加 约 12 约 16 相差不大,ArrayList略优
头部添加 约 1230 约 9 LinkedList 碾压
中间插入 约 340 约 12 LinkedList 碾压
随机访问 约 6 约 6790 ArrayList 碾压
头部删除 约 57 约 8 LinkedList 碾压

🔍 现象分析:

  • ArrayList 在随机访问上无与伦比,因为数组支持 O(1) 索引。

  • LinkedList 在头部/中间插入删除上完胜,因为只需修改节点指针。

  • 尾部添加:两者都很快,但 ArrayList 偶尔扩容会抖动,而 LinkedList 每次都要新建节点。


3.2 创新点:用 LinkedList 实现 LRU 缓存(面试高频)

 LRUCache.java 代码(利用 LinkedList 维护访问顺序 + HashMap 实现 O(1) 查找):

import java.util.LinkedList;
import java.util.HashMap;

class LRUCache<K, V> {
private final int capacity;
private final LinkedList<K> list; // 维护访问顺序
private final HashMap<K, V> map; // 快速查找

public LRUCache(int capacity) {
this.capacity = capacity;
this.list = new LinkedList<>();
this.map = new HashMap<>();
}

public V get(K key) {
if (!map.containsKey(key)) return null;
// 将 key 移到链表头部(表示最近使用)
list.remove(key);
list.addFirst(key);
return map.get(key);
}

public void put(K key, V value) {
if (map.containsKey(key)) {
map.put(key, value);
list.remove(key);
list.addFirst(key);
} else {
if (list.size() >= capacity) {
K oldest = list.removeLast(); // 淘汰最久未使用
map.remove(oldest);
}
list.addFirst(key);
map.put(key, value);
}
}

public void display() {
System.out.println("当前顺序(最近使用的在前): " + list);
}

public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>(3);
cache.put(1, "A");
cache.put(2, "B");
cache.put(3, "C");
cache.display(); // [3,2,1] 注意:因为每次 addFirst,所以最后添加的在最前

cache.get(2); // 访问 2
cache.display(); // [2,3,1]

cache.put(4, "D"); // 容量已满,淘汰最久未使用的 1
cache.display(); // [4,2,3]
System.out.println("获取 key=1: " + cache.get(1)); // null
}
}

 运行结果:

当前顺序(最近使用的在前): [3, 2, 1]
当前顺序(最近使用的在前): [2, 3, 1]
当前顺序(最近使用的在前): [4, 2, 3]
获取 key=1: null

演示了容量为3时,访问 key=2 使其变为最近使用,然后插入 key=4 淘汰最久未使用的 key=1。

💡 创新价值:

  • 展示 LinkedList 在顺序维护场景的天然优势(头尾操作 O(1))。

  • 这是面试中“手写 LRU”的标准解法之一。


3.3 竞赛题:约瑟夫环(使用 ArrayList 模拟)

 Josephus.java 代码及运行结果:

import java.util.ArrayList;
import java.util.List;

public class Josephus {
// 约瑟夫环问题:n 个人围成一圈,数到 m 的人出局,求最后剩下的人的原始编号
public static int josephus(int n, int m) {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= n; i++) list.add(i);
int index = 0;
while (list.size() > 1) {
index = (index + m – 1) % list.size();
list.remove(index);
}
return list.get(0);
}

public static void main(String[] args) {
int n = 10, m = 3;
System.out.println("10个人,数到3出局,最后剩下的是: " + josephus(n, m));
// 验证: 著名的约瑟夫问题结果应为 4(可以手算或网上查证)
}
}

10个人,数到3出局,最后剩下的是: 4

💡 考点:

  • 为什么这里用 ArrayList 而非 LinkedList?因为主要操作是按索引删除(remove(index)),ArrayList 删除的时间复杂度虽然为 O(n),但实际遍历找到该位置很快(连续内存),而 LinkedList 的 remove(index) 需要先遍历到该位置(O(n)),实测更慢。

  • 拓展:如果数据量极大(10万+),应考虑使用 LinkedList 吗?实际上可使用循环链表或数学递推公式(约瑟夫问题最优解是 O(n) 递推)。


📝 四、总结与最佳实践

需求场景推荐 List原因
需要频繁随机访问(如分页查询) ArrayList O(1) 的 get/set
头部/中间频繁插入删除 LinkedList O(1) 的节点操作
主要做尾部添加,且内存敏感 ArrayList 内存更紧凑,尾部添加高效
实现队列/双端队列 LinkedList(也可用 ArrayDeque 更优) 支持快速头尾操作
存储大量数据且需按索引删除中间元素 都不是最优,考虑 LinkedList 或自定义结构 需要根据具体删除频率判断

🎯 开发手册建议:除非明确需要 LinkedList 的头部/中间操作优势,否则默认使用 ArrayList。


📚 参考文献

  • Java集合框架官方文档

  • 《Java面向对象程序设计(第四版)》

  • 《Effective Java》第三版 – 第50条:了解集合框架中的性能差异


🙏 写在最后

如果你觉得这篇文章对你有帮助,请点赞👍 + 收藏⭐ + 评论💬 支持一下!
你的鼓励是我持续输出硬核技术文章的动力。

赞(0)
未经允许不得转载:171主机测评 » ArrayList vs LinkedList:从源码到性能,一张表终结你的选择困难症
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址