在日常Java开发中,我们几乎每天都在使用List 。
而Java中最常用的两个列表实现就是 ArrayList 和 LinkedList。面试官也是,非常喜欢问它们有什么区别。
很多朋友会死记硬背:"ArrayList 查询快,增删慢; LinkedList 查询慢,增删快。"
但你知道为什么吗?今天我们来深入的了解一下。
一、举个例子
你有一排书架:
ArrayList 就像一整排连续的书柜
所有书按顺序紧挨着放,编号 0、1、2......你想拿第5本书时,直接走过去拿就行,非常快!
LinkedList 就像用绳子串连起来的一堆独立小盒子
每个盒子里除了书,还写着前一个盒子在哪和后一个盒子在哪。你想拿第5本书时,得从第一个盒子开始,一个一个顺着绳子找过去。
下面我们再从技术角度进行深入分析。
二、底层结构:数组 vs 链表
ArrayList
ArrayList 底层是动态数组,元素在内存中是连续存放的。
java
// ArrayList简化版原理
public class ArrayList {
private Object[] elementData; // 核心数组
private int size; // 当前元素个数
// 添加元素时,如果数组满了会自动扩容
public void add(Object element) {
if (size == elementData.length) {
// 创建一个1.5倍大的新数组,然后拷贝数据
elementData = Arrays.copyOf(elementData, newCapacity);
}
elementData[size++] = element;
}
}
特点:
- 内存中连续存储,像一排整齐的座位
- 自动扩容,但扩容时需要拷贝所有数据
- 访问速度快,但插入删除可能较慢
LinkedList
LinkedList 的底层是双向链表,元素在内存中是分散存放的,每个元素(节点)都记录着它前后邻居的"地址"。
java
// LinkedList节点结构
class Node {
E item; // 当前节点数据
Node next; // 指向下一个节点
Node prev; // 指向上一个节点
}
// LinkedList简化版原理
public class LinkedList {
private Node first; // 头节点
private Node last; // 尾节点
private int size;
}
每个元素(节点)都保存着前后邻居的信息,就像每个人都知道自己前后是谁。
三、性能对比
我们从三个最常用的操作来看:
1. 随机访问(比如 list.get(100))
ArrayList:O(1)
直接跳到第 100 个位置,秒取!
LinkedList:O(n)
得从头开始数 100 次才能拿到。
结论 :如果你经常通过下标取数据(比如 for 循环遍历),ArrayList要快得多!
2. 在末尾添加/删除(list.add(obj) 或 remove(last))
ArrayList:平均 O(1)
正常情况直接加在最后;但如果空间满了,要扩容(复制整个数组),这时会慢一下(但平均下来还是很快)。
LinkedList:O(1)
直接在尾巴上挂新节点,永远不慢。
结论 :两者在尾部操作都很快,但 ArrayList 还是占优势(因为内存连续,CPU缓存友好)。
3. 在中间或开头插入/删除(比如 list.add(0, obj))
ArrayList:O(n)
插入第 0 位?后面所有元素都要往后挪一位!删也一样,前面空了,后面的要往前挤。
LinkedList:理论上 O(1),但实际常是 O(n)
虽然插入本身只要改指针,但你要先找到第 0 个节点啊!找的过程就要从头遍历,所以整体还是 O(n)。
重要提醒: 很多人以为 LinkedList 在中间插入更快,其实只有当你已经有那个位置的引用时才快(比如用 Iterator 遍历时删除)。
如果只是用 list.add(500, obj),它照样要先找第 500 个节点,速度并不比 ArrayList 快!
四、内存占用
ArrayList
只存数据和一点预留空间(比如容量 10,只用了 6,那有 4 个空位)。
LinkedList
每个元素都要额外存两个指针(指向前一个和后一个节点),内存开销大很多!
举个例子:存1000个 Integer 对象:
ArrayList:约4KB(假设每个 int 4 字节)LinkedList:约4KB(数据)+ 8KB(指针,每个指针 8 字节 × 2 × 1000)= 12KB+
结论 :ArrayList 更省内存,尤其存大量小对象时优势更加明显。
五、其他细节补充
是否支持"快速随机访问"?
ArrayList实现了RandomAccess接口(这是一个标记接口,告诉程序员:"我支持快速随机访问")。LinkedList没有实现它。
有些工具类(如 Collections.binarySearch)会根据这个接口选择不同算法,进一步优化性能。
线程安全吗?
都不安全! 多线程环境下直接用会出问题。
如果需要线程安全,可以用:
java
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
或者用 CopyOnWriteArrayList(适合读多写少的场景)。
LinkedList 还能当队列用!
LinkedList 实现了 Deque 接口,可以当双端队列用:
java
LinkedList<String> queue = new LinkedList<>();
queue.addFirst("A");
queue.addLast("B");
String first = queue.removeFirst(); // A
这时候它的链表结构就非常合适!
六、开发时该用哪个?
| 使用场景 | 推荐选择 | 原因 |
|---|---|---|
| 经常通过下标访问(如 for 循环、get(i)) | ArrayList | 随机访问快 |
| 主要在末尾增删(如日志记录、追加数据) | ArrayList | 简单高效,缓存友好 |
| 需要频繁在已知位置插入/删除(如用 Iterator 遍历时删元素) | LinkedList | 指针操作快 |
| 要实现队列、栈、双端队列 | LinkedList | 原生支持 addFirst/removeLast |
| 内存紧张 or 存大量小对象 | ArrayList | 内存更紧凑 |
一般情况下,默认选 ArrayList 就对了!
只有在明确需要链表特性(如双端操作)时,才考虑 LinkedList。
总结
ArrayList 底层是数组,内存连续,访问快(O(1)),中间增删慢(O(n))。
LinkedList 底层是链表,内存分散,访问慢(O(n)),头尾增删快(O(1))。
日常开发中,优先选 ArrayList 准没错!
只有当你真的需要频繁在列表头尾增删、或者用它当队列/栈时,再考虑 LinkedList。
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》
《Java 开发必看:什么时候用 for,什么时候用 Stream?》