带团队这几年,我面试过不少Java候选人,也帮新人做过代码评审。只要聊到集合,我几乎每次都会问一句:“LinkedList真的插入比ArrayList快吗?”得到的回答十有八九是“对啊,链表插入不用搬数组”。可再追问一句“那你往中间插入100万次试试”,很多人就愣住了。这个Java基础容器从JDK 1.2活到现在,底层是双向链表,同时实现List、Deque两个接口,所以它天生有一种“两头强、中间弱”的特质。这篇文章我不打算只讲教科书结论,而是从源码执行路径、内存模型、真实性能对比、高频面试追问这几个维度,把LinkedList彻底拆开。无论你是刚开始啃Java集合源码,还是在准备Java面试题,又或者是在项目里纠结到底该用哪个容器,这篇都值得从头看到尾。
1. 先从底层说起:LinkedList在数据结构上是什么样
1.1 双向链表的基本形态
数据结构课本里讲过,链表和数组是两种相反的组织方式。数组在内存里是一块连续空间,通过下标加偏移量就能直接找到元素;链表则反过来,每个元素都是一个独立的节点,节点之间靠引用串联。LinkedList用的是双向链表,也就是说,每个节点除了保存自己的数据,还有两个引用,一个指向后一个节点,指向前一个节点。
在Java里,节点的定义大致是这么一个内部类:每个Node持有三个东西,item是真正存的对象,next指向后继节点,prev指向前驱节点。头节点的prev是null,尾节点的next也是null。LinkedList内部保存first和last两个引用,分别指向链表的头尾。正是因为同时持有头尾引用,它才能在O(1)时间内完成头插和尾插。
这个概念看起来简单,但它是理解LinkedList所有行为的地基。很多面试中考察的问题,比如为什么中间插入慢、为什么不能随机访问、为什么内存占用大,归根结底都可以从这张双向链表结构图上找到答案。
1.2 JDK源码里LinkedList的“多重身份”
打开JDK源码,LinkedList的类声明是这样的:
java复制public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
它继承了AbstractSequentialList,这个父类名字里的“Sequential”已经暗示了它的遍历风格。重点在于它同时实现了List接口和Deque接口。List接口意味着它可以像ArrayList一样按索引访问、按索引插入;Deque接口意味着它又能当双端队列、栈来用。所以LinkedList本质上是一个“什么都沾一点,但都不是最优”的容器。
类里最核心的三个成员变量是:
java复制transient int size = 0;
transient Node<E> first;
transient Node<E> last;
size记录节点总数,first指向第一个节点,last指向最后一个节点。注意这两个引用是transient的,序列化时需要手动遍历整个链表。构造方法只有两个:一个是无参构造,直接创建一个空链表;另一个是传入Collection的构造,先把链表置空,再通过addAll把集合里的元素逐个追加到尾部。
很多面试者在回答LinkedList原理时能说出“双向链表”四个字,但再往深一层,问他Node节点里有什么字段,就说不清楚了。这说明他看的是别人总结的“八股文”,没真正翻过源码。我的建议是,源码里这段Node定义不长,花十分钟看一下,收获远比背十道面试题大。
1.3 实现了Deque,LinkedList到底多了什么本事
Deque是“Double Ended Queue”的缩写,也就是双端队列。它定义了addFirst、addLast、removeFirst、removeLast、getFirst、getLast、offerFirst、offerLast、pollFirst、pollLast、peekFirst、peekLast这一系列方法。LinkedList正因为实现了Deque,所以这些方法全都具备。
在实际开发里,我们经常会把LinkedList直接赋值给Deque或Queue接口来用。比如需要实现一个先进先出的队列,有人会写Queue queue = new LinkedList<>()。这没有错,因为LinkedList实现了Queue接口。但这里有个隐藏知识点:如果你用Queue类型接收,那么只调用add、poll、peek,实际执行路径仍然走的是LinkedList的尾插、头删逻辑,复杂度是O(1)。同时还能用push、pop方法模拟栈行为。这种“一个类扮演多种角色”的特性,给了代码很强的灵活性,但也容易让人忽略底层每个操作的真实成本。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心操作详解:一次add和remove背后发生了什么
2.1 尾部新增元素,为什么能做到O(1)
先看最常用的add(E e)方法。它没有参数,逻辑就是往链表尾部追加一个节点。源码最终会调用linkLast方法,作用是把一个新节点挂在last后面。因为LinkedList直接保存了last引用,所以尾部新增不需要遍历任何节点,只需要做这么几件事:创建一个新节点,把它的prev指向旧的last,把旧last的next指向新节点,再把LinkedList的last更新成新节点。如果链表本来为空,就让first也指向新节点。整个过程不涉及数组复制,时间复杂度O(1)。
同样,addFirst和push方法走的是linkFirst,往链表头部插入节点,过程是对称的:新节点的next指向旧的fi
