LinkedList 源码逐行读:节点结构、头尾插入、双向遍历实现
LinkedList 源码逐行读:节点结构、头尾插入、双向遍历实现
关键词:Java集合、LinkedList、双向链表、节点结构、头尾插入、双向遍历、源码、面试
适合人群:Java初中高级工程师·面试冲刺·代码调优·架构设计
阅读时长:35 min(≈ 5300字)
版本环境:JDK 17(源码行号对应 jdk-17+35)
1. 开场白:面试三连击,能抗算我输
“LinkedList 的节点长啥样?为什么内部类叫 Node 而不是 Entry?”
“头插和尾插时间复杂度都是 O(1),但哪种更省 CPU?”
“双向遍历用 ListIterator,怎么做到一边向前一边向后?”
阿里 P7 面完 100 人,90% 画不出 Node 前后指针。
线上事故:某 RPC 框架用 LinkedList 做连接池,每秒 5k add/remove,YoungGC 从 30 ms 涨到 300 ms,CPU 占用高 40%,定位发现 Node 对象 200 万个。
背完本篇,你能徒手画出内存布局图,手写无锁双向链,顺便给出 3 种场景选型,让 GC 降 80%。
2. 知识骨架:LinkedList 全景一张图
LinkedList
├─private static class Node<E> { E item; Node<E> next; Node<E> prev; }
├─transient Node<E> first;
├─transient Node<E> last;
├─transient int size = 0;
└─implements List, Deque, Queue
核心操作:
linkFirst()
→ linkLast()
→ linkBefore()
→ unlink()
→ listIterator()
3. 身世档案:核心参数一表打尽
字段/参数 | 含义 | 默认值/备注 |
---|---|---|
first | 头节点引用 | null |
last | 尾节点引用 | null |
size | 元素个数 | 0 |
Node | 内部静态类 | 前驱 + 数据 + 后继 |
modCount | 结构性修改计数 | 迭代器快速失败 |
4. 原理解码:源码逐行,行号指路
4.1 节点结构:私有静态内部类(行号 910)
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;}
}
静态内部类:不持有外部类引用,节省 1 个字节开销。
4.2 头插:linkFirst()(行号 155)
private void linkFirst(E e) {final Node<E> f = first;final Node<E> newNode = new Node<>(null, e, f);first = newNode;if (f == null)last = newNode; // 空链表elsef.prev = newNode;size++;modCount++;
}
4.3 尾插:linkLast()(行号 170)
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;elsel.next = newNode;size++;modCount++;
}
4.4 中间插入:linkBefore()(行号 185)
void linkBefore(E e, Node<E> succ) {final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode;if (pred == null)first = newNode;elsepred.next = newNode;size++;modCount++;
}
4.5 删除节点:unlink()(行号 220)
E unlink(Node<E> x) {final E element = x.item;final Node<E> next = x.next;final Node<E> prev = x.prev;if (prev == null) {first = next;} else {prev.next = next;x.prev = null;}if (next == null) {last = prev;} else {next.prev = prev;x.next = null;}x.item = null; // help GCsize--;modCount++;return element;
}
注意:item 置 null,避免游离节点导致内存泄漏。
5. 实战复现:3 段代码 + GC 对比
5.1 头尾插入性能对比
int N = 1_000_000;
// 头插
LinkedList<Integer> ll = new LinkedList<>();
long t1 = System.nanoTime();
for (int i = 0; i < N; i++) ll.addFirst(i);
long t2 = System.nanoTime();
System.out.println("addFirst: " + (t2 - t1) / 1_000_000 + " ms");// 尾插
ll = new LinkedList<>();
t1 = System.nanoTime();
for (int i = 0; i < N; i++) ll.addLast(i);
t2 = System.nanoTime();
System.out.println("addLast: " + (t2 - t1) / 1_000_000 + " ms");
输出(JDK 17,i5-11400):
addFirst: 78 ms
addLast: 72 ms
尾插略快:少一次 first.prev
写屏障。
5.2 遍历对比:普通 for vs 增强 for vs 双向迭代器
ListIterator<Integer> it = ll.listIterator();
long t1 = System.nanoTime();
while (it.hasNext()) it.next();
long t2 = System.nanoTime();
System.out.println("forward: " + (t2 - t1) / 1_000_000 + " ms");while (it.hasPrevious()) it.previous();
long t3 = System.nanoTime();
System.out.println("backward: " + (t3 - t2) / 1_000_000 + " ms");
双向迭代器前后遍历均 O(n),无额外内存。
5.3 Node 对象数量压测
// JProfiler 观察
LinkedList<Byte> list = new LinkedList<>();
for (int i = 0; i < 5_000_000; i++) list.add((byte)1);
结果:
Node 实例 500 万,内存 ≈ 500w * 24 B = 114 MB
同等数据 ArrayList<byte[]>
仅 5 MB,差 20 倍。
6. 线上事故:LinkedList 做队列导致 Node 爆炸
背景
RPC 连接池用 LinkedList<Channel>
保存空闲连接,高峰 5 k QPS。
现象
YoungGC 从 30 ms 涨到 300 ms,CPU 占用 +40%。
根因
连接频繁借还,Node 对象每秒新建 1 万,GC 压力巨大。
复盘
- 压测复现:Node 对象 200 万,GC 耗时 10 倍。
- 修复:替换为
ArrayDeque<Channel>
,Node 对象消失。 - 防呆清单:
- 高并发队列优先使用 ArrayDeque / ConcurrentLinkedQueue;
- 对象池禁用 LinkedList。
7. 面试 10 连击:答案 + 行号
问题 | 答案 |
---|---|
1. LinkedList 节点结构? | 静态内部类 Node,prev/item/next(行号 910) |
2. 头插和尾插谁更快? | 尾插少一次 prev 写,略快(行号 170) |
3. 中间插入如何定位? | 先根据 index < size>>1 决定从前还是后遍历(行号 389) |
4. 删除节点为什么 item 置 null? | help GC,避免游离引用(行号 235) |
5. 线程安全吗? | 否,可用 Collections.synchronizedList |
6. 能用 fori 随机访问吗? | 可以,但每次需遍历,O(n) |
7. 内存占用对比 ArrayList? | 每个元素额外 Node 24 B,100 万元素多 20 倍 |
8. 如何实现双向迭代? | ListIterator 前后指针(行号 885) |
9. 队列场景选型建议? | 高并发用 ArrayDeque,并发用 ConcurrentLinkedQueue |
10. 还能用 LinkedList 吗? | 低并发、频繁头尾插入、需 null 元素时可考虑 |
8. 总结升华:一张脑图 + 三句话口诀
[脑图文字版]
中央:LinkedList
├─Node:prev/item/next
├─头插:linkFirst
├─尾插:linkLast
├─删除:unlink + help GC
└─遍历:双向 ListIterator
口诀:
“节点静态省引用,头尾 O(1) 真轻松;Node 爆炸要当心,ArrayDeque 替代雄。”
9. 下篇预告
下一篇《HashMap 源码逐行读:hash 方法、冲突链表、红黑树阈值、扩容死链》将带你手写红黑树旋转、复现 JDK 7 死链环路,敬请期待!
10. 互动专区
你在生产环境踩过 LinkedList Node 内存坑吗?评论区贴出 GC 图 / 堆 Dump,一起源码级排查!