Java 中的 LinkedList
—— 这是一个基于双向链表的集合类,与 ArrayList
的数组结构形成鲜明对比。
一、LinkedList 的底层结构
1. 底层实现:双向链表
LinkedList
是基于 双向链表(Doubly Linked List) 实现的,每个节点包含:- 数据(
element
) - 前驱节点指针(
prev
) - 后继节点指针(
next
)
- 数据(
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
LinkedList
维护两个头尾指针:first
:指向链表的第一个节点last
:指向链表的最后一个节点
二、LinkedList 的核心特性
特性 | 说明 |
---|---|
有序 | 元素按插入顺序存储 |
可重复 | 允许重复元素 |
动态大小 | 插入/删除时自动调整链表长度 |
线程不安全 | 多线程下需手动同步 |
允许 null | 可以存储 null 值 |
双向链表 | 支持正向和反向遍历 |
三、时间复杂度对比(与 ArrayList 对比)
操作 | LinkedList | ArrayList |
---|---|---|
get(index) |
O(n) | O(1) ⚡️ |
add(E e) |
O(1)(尾部) | 均摊 O(1)(偶尔扩容 O(n)) |
add(index, E e) |
O(n)(需定位) | O(n)(需移动元素) |
remove(index) |
O(n)(需定位) | O(n)(需移动元素) |
contains(E e) |
O(n) | O(n) |
内存占用 | 高(每个节点额外存储 prev/next) | 低(仅存储元素) |
一句话:
- 随机访问:
ArrayList
更快 - 中间插入/删除:
LinkedList
更快 - 尾部操作:两者接近(
LinkedList
无扩容开销)
四、LinkedList vs ArrayList 的核心区别
对比项 | LinkedList | ArrayList |
---|---|---|
底层结构 | 双向链表 | 动态数组 |
随机访问 | O(n) | O(1) |
插入/删除(中间) | O(1)(已知位置) | O(n)(需移动元素) |
内存占用 | 高(每个节点有 prev/next 指针) | 低(仅存储元素) |
适用场景 | 频繁中间插入/删除 | 频繁随机访问、尾部操作 |
线程都不安全 | ||
是否支持 RandomAccess | 支持(但慢) | 支持(且快) |
如果业务中主要是频繁在中间插入或删除,且不常随机访问,
LinkedList
更合适;如果是频繁按索引查询或尾部操作,ArrayList
性能更好。
五、LinkedList 的常见操作源码解析
1. 添加元素(尾部)
public boolean add(E e) {
linkLast(e); // 直接添加到尾部
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null) {
first = newNode; // 如果链表为空
} else {
l.next = newNode;
}
size++;
modCount++;
}
- 时间复杂度:O(1)
2. 插入元素(指定位置)
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size) {
linkLast(element);
} else {
Node<E> succ = node(index); // 找到指定位置的节点
Node<E> pred = succ.prev;
Node<E> newNode = new Node<>(pred, element, succ);
succ.prev = newNode;
if (pred == null) {
first = newNode;
} else {
pred.next = newNode;
}
size++;
modCount++;
}
}
- 时间复杂度:O(n)(需定位节点)
六、LinkedList 的线程安全问题
1. 线程不安全
LinkedList
不是线程安全的。多线程环境下,如果多个线程同时修改(如add/remove
),可能导致数据不一致或异常。
2. 如何保证线程安全?
方法 | 说明 | 使用场景 |
---|---|---|
Collections.synchronizedList() |
包装成同步列表 | 通用 |
CopyOnWriteArrayList |
写时复制,读不加锁 | 读多写少 |
手动加锁 | 使用 synchronized 或 ReentrantLock |
灵活控制 |
// 方式1:同步包装
List<String> syncList = Collections.synchronizedList(new LinkedList<>());
// 方式2:使用 CopyOnWriteArrayList
List<String> cowList = new CopyOnWriteArrayList<>();
对比:
synchronizedList
:所有操作加锁,性能较低。CopyOnWriteArrayList
:写操作复制整个数组,开销大,但读操作无锁,适合“读多写少”场景(如监听器列表)。
七、LinkedList 的 fail-fast 机制
LinkedList
的迭代器同样基于modCount
实现 fail-fast 机制。- 原理:
- 迭代器创建时保存
expectedModCount = modCount
。 - 每次
next()
前检查modCount == expectedModCount
,不一致则抛出ConcurrentModificationException
。
- 迭代器创建时保存
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
- 单线程下,边遍历边修改也会触发异常。
- 正确删除方式:使用
Iterator.remove()
,它会同步更新expectedModCount
。
八、LinkedList 的一些理论问题
1. 为什么 LinkedList 的随机访问效率低?
因为
LinkedList
是基于链表实现的,访问第n
个元素需要从头节点或尾节点遍历到目标位置,时间复杂度为 O(n)。
// LinkedList 的 get(index) 实现
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// 如果 index 在前半部分,从头开始遍历
// 否则从尾开始遍历
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
2. LinkedList 和 ArrayList 的适用场景?
场景 | 推荐使用 |
---|---|
频繁随机访问 | ArrayList |
频繁中间插入/删除 | LinkedList |
尾部频繁操作 | ArrayList (无扩容开销) |
读多写少 | CopyOnWriteArrayList |
3. LinkedList 的内存占用为什么比 ArrayList 高?
LinkedList
的每个节点除了存储元素外,还需要存储prev
和next
指针,导致内存开销比ArrayList
大。
4. 如何高效删除 LinkedList 中的所有值为 100 的元素?
推荐方式:
// 使用迭代器(避免并发修改异常)
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
if (it.next() == 100) {
it.remove(); // ✅ 安全删除
}
}
// 或使用 removeIf(Java 8+)
list.removeIf(x -> x == 100);
九、LinkedList 的优缺点总结
优点 | 缺点 |
---|---|
插入/删除效率高(O(1),已知位置) | 随机访问效率低(O(n)) |
尾部添加无需扩容 | 内存占用高(每个节点有指针) |
支持双向遍历(ListIterator ) |
不适合频繁随机访问的场景 |
十、实际应用
何时选择
LinkedList
?- 需要频繁在头部或中间插入/删除元素(如任务队列、历史记录)。
- 不需要频繁随机访问元素。
何时避免
LinkedList
?- 需要频繁按索引访问元素(如日志查询、缓存)。
- 内存敏感场景(链表节点指针占用额外内存)。
十一、得到的技巧
题目:如何高效合并两个有序的 LinkedList?
参考:
// 使用双指针合并两个有序链表
public static LinkedList<Integer> mergeTwoSortedLists(LinkedList<Integer> l1, LinkedList<Integer> l2) {
LinkedList<Integer> result = new LinkedList<>();
Node<Integer> p1 = l1.getFirstNode(), p2 = l2.getFirstNode();
while (p1 != null && p2 != null) {
if (p1.item <= p2.item) {
result.add(p1.item);
p1 = p1.next;
} else {
result.add(p2.item);
p2 = p2.next;
}
}
// 添加剩余元素
while (p1 != null) {
result.add(p1.item);
p1 = p1.next;
}
while (p2 != null) {
result.add(p2.item);
p2 = p2.next;
}
return result;
}
解释:
- 时间复杂度:O(m + n)
- 空间复杂度:O(m + n)
最后总结:LinkedList 核心要点
考点 | 答案 |
---|---|
底层结构 | 双向链表 |
随机访问性能 | O(n)(需遍历) |
插入/删除性能 | O(1)(已知位置) |
内存占用 | 高(每个节点有指针) |
与 ArrayList 的区别 | 链表 vs 数组,插入删除 vs 随机访问 |
线程安全 | 否,需用 synchronizedList 或 CopyOnWriteArrayList |
fail-fast 机制 | 与 ArrayList 一致,迭代时修改会抛异常 |
适用场景 | 频繁中间插入/删除,不适合随机访问 |