LinkedList可能是Java集合框架里最容易被“背答案”的一个类。网上随便一搜就是“ArrayList查快增删慢,LinkedList查慢增删快”,然后面试也这么答,写完一个for循环get(i)之后被性能打脸也不知道为什么。这次我想用一篇完整的源码笔记,把LinkedList从类声明、Node结构、add/get/remove核心方法,到它作为Deque和顺序集合的另一面,逐层拆开来讲。通篇以JDK 8的经典实现为主参照,顺带聊JDK后续版本出现的接口变化,适合正准备啃集合源码的Java开发者,也适合面试前想真正搞懂而不是死记结论的人。
1. LinkedList到底是个什么样的“链表”
1.1 从类声明反推设计意图
打开源码第一行就能看到一个有意思的信息:
java复制public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
它同时实现了List<E>和Deque<E>两个接口,这决定了LinkedList不能只被当作“列表”来理解。Deque是双端队列,意味着它能从头部加、从头部取、从尾部加、从尾部取,所以LinkedList天生就可以当栈、当队列、当双端队列使用。这跟ArrayList有本质区别,ArrayList只是List,想实现栈和队列要么自己包一层,要么用ArrayDeque。
AbstractSequentialList这个父类也很关键。它名字里的“Sequential”不是“顺序访问”的意思,而是“按顺序逐个访问”。这个类为“迭代式访问”的列表提供了骨架实现,把get(int index)、set(int index, E element)、add(int index, E element)、remove(int index)这些按下标操作的方法,统一转成了基于ListIterator的迭代逻辑。
问题来了:既然父类已经用迭代器实现了按下标操作,为什么LinkedList还要自己重写一遍?因为父类的实现方式是“从链表头部开始一个节点一个节点往后找”,而LinkedList有首尾两个指针,可以折半查找,两边同时往中间摸,但ArrayList没有这个能力。它自己重写这些方法,本质是在用“结构信息”换性能。
从类声明还能推出一个重要结论:LinkedList在绝大多数实现里不是一个普通的“单链表”。很多初学者脑中想象的链表是“当前节点只存next”,像手拉手一串,但LinkedList内部是双向的。它是双向链表,每个节点都有前驱指针prev和后继指针next。
1.2 Node:整个链表的最小积木
LinkedList内部用一个静态内部类表示节点,JDK 8里的定义是这样的:
java复制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;
}
}
三个字段分别对应:当前节点存的值、后继节点引用、前驱节点引用。链表的“串”不是靠连续内存实现的,而是靠每个节点的next/prev互相指认。这就引申出两个关键结论:
- 内存不是连续分配的,所以不需要像ArrayList那样预留连续空间,也不需要“扩容”和“搬移”元素。
- 想从某个节点找到相邻节点,代价是O(1),只要改一下next和prev的指向就能插入或删除。
不过,内存不连续也带来了明显的劣势:无法通过“起始地址 + 索引 * 对象大小”直接算出某个位置的内存地址,想按下标找到节点必须从头或尾沿途走一遍,这也是后面讲node(int index)时为什么需要折半搜索的根源。
我一直建议真正读源码时先看内部类,内部类的形态基本决定了这个集合的底层策略。看到Node带prev和next,心里就应该浮现出一幅“双向链表图”,后续所有方法都是在维护这张图不破。
1.3 三个字段维系整张链
LinkedList实例本身的字段非常少,JDK 8中就是:
java复制transient int size = 0;
transient Node<E> first;
transient Node<E> last;
没有数组,没有Map,也没有其他辅助存储。first指向链表的头节点,last指向尾节点,size记录节点总数。LinkedList能实现O(1)头尾插入,靠的就是first和last这两个哨兵一样存在的指针,每次在头部或尾部插入时只需要局部改几个引用,然后更新first/last即可。
transient的作用在这里需要多说一句。LinkedList重写了writeObject和readObject,序列化时只序列化size和每个节点的item,不序列化节点之间的next/prev引用,因为链表的引用关系在反序列化后必然失效,硬序列化结构信息不仅浪费空间,还会让不同JVM版本之间的序列化结果更难兼容。自定义序列化是这个类实现java.io.Serializable时刻意做的优化,读源码时留意到这一步,会对“为什么LinkedList要看重写序列化方法”有更具体的认识。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 新增元素:从add(E e)到add(int index, E element)
2.1 add(E e):永远挂在尾部
平时最常用的add(E e),源码内部
