LinkedList接口源码解读
LinkedList 接口源码解读
前言
因为追求质量,所以写的较慢。大概在接下来的三天内会把LinkedList源码解析出完。已经出完啦!废话不多说,正片开始! (文章最后面有后记哦~)
大家都知道,LinkedList
是在Java
底层中是由 双向链表 实现的。那么今天我们就来阅读下源码,看看Java
是如何实现这个功能强大的集合。
注意: JDK1.6 之前为双向循环链表,JDK1.7 取消了循环,只使用了双向链表。
首先,我们看一下LinkedList
类定义
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, Serializable {// transient 关键字表示被该关键字修饰的属性不会被序列化。transient int size;transient Node<E> first;transient Node<E> last;private static final long serialVersionUID = 876323262645176354L;
}
一. 继承和实现关系
LinkedList
类继承了 AbstractSequentialList
,而AbstractSequentialList
又继承了 AbstractList
。
- 我们知道
ArrayList
也继承了AbstractList
,所以LinkedList
中的大部分方法会和ArrayList
相似。
LinkedList
类实现了List
、Deque
、Cloneable
、Serializable
接口。
-
List
: **表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 ** -
Deque
: 继承自Queue
接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。 -
Cloneable
:Cloneable
注解是一个标记接口,我们点进去会发现它并没有任何方法。此接口表明LinkedList
具有拷贝能力,可以进行深拷贝或浅拷贝操作 。package java.lang;public interface Cloneable { }
-
Serializable
: 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
二、属性实现
LinkedList
中的元素属性是通过 Node
类来声明的:
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;}
}
三、初始化
在进入正题之前,我先来给各位科普下 transient
关键字的作用是什么?
相信大家在阅读源码的过程中,很多属性的定义前都会加transient
关键字。
transient
是Java
语言的关键字,用来表示一个成员变量不是该对象序列化的一部分。当一个对象被序列化的时候,transient
所修饰的变量的值不包括在序列化的结果中。
transient int size; // 链表节点的个数,默认为 0
transient Node<E> first; // 指向链表的第一个节点
transient Node<E> last; // 指向链表的最后一个节点
private static final long serialVersionUID = 876323262645176354L;// 无参构造方法,调用此构造方法创建的集合长度默认为 0
public LinkedList() {this.size = 0;
}// 有参构造方法,传入一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
public LinkedList(Collection<? extends E> c) {this();this.addAll(c);
}
在调用有参构造方法时,LinkedList
会调用两个方法this()
、addAll()
。
-
this()
:调用有参构造的时候,首先会去调用无参构造,创建一个默认大小为 0 的集合。 -
addAll()
:底层就是将传过来的集合类型转换成Object[]
数组,然后依次遍历数组中的每个元素,添加到链表上。
源码:
// 将集合类型保存到链表中
public boolean addAll(Collection<? extends E> c) {// 底层调用了重载后的 addAll(int index, Collection<? extends E> c) return this.addAll(this.size, c); // 等同于 return this.addAll(0, c);
}
// addAll() 最终实现类
public boolean addAll(int index, Collection<? extends E> c) {// 索引越界检查,条件为:index >= 0 && index <= this.size// 满足条件返回true,否则报错this.checkPositionIndex(index);Object[] a = c.toArray(); // 转换成 Object[]int numNew = a.length;// 非空校验if (numNew == 0) {return false;} else {// 核心逻辑!!// pred 可以理解为记录节点位置的指针。每有一个新节点就会更新 pred 的值// succ 是一个标记节点,循环结束后会判断当前 succ 是否为 nullNode pred; Node succ;if (index == this.size) { // 调用有参构造方法时,size = 0 并且 index = 0,所以会进入下方的逻辑succ = null;pred = this.last; // 此时,链表为空,所以 last = null。可以转换成 pred = null} else {succ = this.node(index);pred = succ.prev;}Object[] var7 = a;int var8 = a.length;// 循环数组,依次往链表中添加元素for(int var9 = 0; var9 < var8; ++var9) {Object o = var7[var9]; // 取出指定索引的值E e = o;// LinkedList 底层是由双向链表实现的// 此语句的意思为:创建一个前缀指针指向 pred,值为 e,后缀指针指向 null 的新节点Node<E> newNode = new Node(pred, e, (Node)null);if (pred == null) { // 第一次循环时 pred 肯定为 null,进入下方逻辑// 表明当前 newNode 是链表的第一个节点// 设置 LinkedList 的 first 指针指向此新节点this.first = newNode; } else { // 除了第一次进入循环,后面的每次循环都会进下方逻辑// 依次往链表中添加元素 1 -> 2 -> 3pred.next = newNode;}// 更新 pred 的值pred = newNode;}if (succ == null) {// 此时的 pred 节点是当前链表的最后一个值// 设置 LinkedList 的 last 指针指向此循环结束后的最后一个 pred 节点this.last = pred; } else {pred.next = succ;succ.prev = pred;}// 无关操作 更新链表中的节点数// size 是链表所用来记录当前节点值的// modCount 是 List 集合存储元素个数的值// 因为 LinkedList 继承了 AbstractSequentialList,所以 List 的操作它也都有this.size += numNew;++this.modCount;return true;}}
四、插入元素
LinkedList
除了实现了 List
接口相关方法,还实现了 Deque
接口的很多方法,所以我们有很多种方式插入元素。
下面我将以 List
、Queue
、Stack
这三种数据结构分开带大家阅读源码。
可能会有朋友好奇,Stack
是什么?Stack
是栈这种数据结构,他的特点是只有一端能进出,这就导致栈的特点是先进后出,这和队列Queue
的先进后出恰好相反
那朋友们又要好奇了,LinkedList
在他的类方法定义上也没看到一个叫 Stack
的接口啊,它怎么能当作栈呢?
大家要记住,数据结构底层都是相通的。栈有多种实现方式,可以通过单向链表实现栈
、数组实现栈
、双向链表实现栈
,而 LinkedList
正是通过双向链表来实现了对栈的操作
/*** 为了让大家能够明白上面那句话的意思,我用单向链表实现了一个栈,各位可以看看代码* @Description 单向链表实现栈* @Author Mr.Zhang* @Date 2024/8/2 14:40* @Version 1.0*/
public class LinkedListStack<E> implements Iterable<E> {// 容量private int capacity;private int size; // 栈中元素的个数// 头指针指向哨兵节点private Node<E> head = new Node<>(null, null);public LinkedListStack(int capacity) {this.capacity = capacity;}static class Node<E> {E value;Node<E> next;public Node(E value, Node<E> next) {this.value = value;this.next = next;}}/*** 向栈顶压入元素** @param value 待压入值* @return 压入成功返回 true,否则返回 false*/@Overridepublic boolean push(E value) {if (isFull()) {return false;}head.next = new Node<E>(value, head.next);size++;return true;}/*** 从栈顶弹出元素** @return 栈非空返回栈顶元素,栈为空返回 null*/@Overridepublic E pop() {if (isEmpty()) {return null;}E value = head.next.value;head.next = head.next.next;size--;return value;}/*** 返回栈顶元素,不弹出** @return 栈非空返回栈顶元素,栈为空返回 null*/@Overridepublic E peek() {if (isEmpty()) {return null;}return head.next.value;}/*** 检查栈是否为空** @return 空返回 true,否则返回 false*/@Overridepublic boolean isEmpty() {return head.next == null;}/*** 检查栈是否已满** @return 满了返回 true,否则返回 false*/@Overridepublic boolean isFull() {return capacity == size;}/*** 方便我用 增强for循环 测试*/@Overridepublic Iterator<E> iterator() {return new Iterator<E>() {Node<E> p = head.next;@Overridepublic boolean hasNext() {return p != null;}@Overridepublic E next() {E value = p.value;p = p.next;return value;}};}
}
-
List
add(E e)
: 用于在LinkedList
的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为O(1)
。addAll(int index, Collection<? extends E> c)
:用于通过给定的集合类型数据创建一个LinkedList
。 (此源码已经在上面了,下方不重复解读)add(int index, E element)
:用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为O(n)
。addFirst(E e)
: 用于在LinkedList
的头部插入元素,即将新元素作为链表的第一个元素,时间复杂度为O(1)
。底层调用LinkedFirst(E e)
来插入元素。addLast(E e)
: 用于在LinkedList
的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为O(1)
。 底层调用linkLast(E e)
来插入元素。
源码:
// 在 LinkedList 尾部添加元素
public boolean add(E e) {this.linkLast(e);return true;
}// 向 LinkedList 尾部添加元素的底层实现方法
void linkLast(E e) {Node<E> l = this.last; // 获取最后一个节点// 根据传入的 e 创建新节点,新节点的前一个指针指向之前链表的最后一个节点Node<E> newNode = new Node(l, e, (Node)null); this.last = newNode; // 更新新节点的值if (l == null) { // 判断如果当前链表为空this.first = newNode; // 则将 LinkedList 的 last 指针也指向这个新节点} else {l.next = newNode; // 更新 l 的 next 指向的节点}// 无关操作 更新链表中的节点数++this.size;++this.modCount;
}// 根据指定的索引插入元素
public void add(int index, E element) {// 索引越界检查,条件为:index >= 0 && index <= this.size// 满足条件返回true,否则报错this.checkPositionIndex(index);if (index == this.size) { // 如果传递过来的 index 正好等于数组长度,则证明是向链表最后添加元素this.linkLast(element);} else {// 核心代码this.linkBefore(element, this.node(index)); // this.node(index):找到待插入索引目前存在的节点元素}
}// 根据指定的索引插入元素的底层实现方法
void linkBefore(E e, Node<E> succ) {// 定义一个节点元素保存 succ 的 prev,也就是它的前一节点信息Node<E> pred = succ.prev;// 初始化节点,并指明前驱和后继节点Node<E> newNode = new Node<>(pred, e, succ);// 将 succ 节点前驱引用 prev 指向新节点succ.prev = newNode;// 判断前驱节点是否为空,为空表示 succ 是第一个节点if (pred == null) {this.first = newNode; // 当前 newNode 节点就是新的首节点,所以要让 first 指向 newNode} else {pred.next = newNode;}// 无关操作 更新链表中的节点数++this.size;++this.modCount;
}// 向链表尾部添加元素
void linkLast(E e) {// 取出链表的尾指针指向的元素Node<E> l = this.last;// 根据给定的 e 创建新节点,此时新节点的前缀节点为原先尾指针指向的元素,后序节点为 nullNode<E> newNode = new Node(l, e, (Node)null);// 更改为尾指针指向的元素this.last = newNode;if (l == null) { // 如果尾指针指向的元素为 null,则证明当前链表为空,此时新节点就是头节点。this.first = newNode;} else {l.next = newNode; // 否则,将原来的尾指针的 next 指向新节点}// 无关操作 更新链表中的节点数++this.size;++this.modCount;
}
-
Queue
offer(E e)
:队列专用的方法。用于向队列尾部插入元素,即将新元素作为队列的最后一个元素。底层是调用链表的linkLast(E e)
实现的插入操作。offerFirst(E e)
:队列专用的方法。用于向队列头部添加元素。底层是调用链表的linkFirst(E e)
实现的插入操作。offerLast(E e)
:队列专用的方法。用于向队列尾部添加元素。底层是调用链表的linkLast(E e)
实现的插入操作。
总结:
Queue
队列在进行插入操作时,大部分情况下我们会直接使用offer(E e)
方法,他们底层都是调用List
接口定义的方法,只不过二次封装了。 -
Stack
push(E e)
:栈专用的方法。用于向栈顶压入元素。底层是调用链表的linkFirst(E e)
实现的插入操作。
后记
在这篇 LinkedList 源码解读中我只带大家读完了插入
操作。那么还有删除
、修改
操作,我希望大家可以自己去读读源码自己搞懂。只有自己亲自读过的源码才能真正的掌握。大家要是在读源码的过程中有什么方法没看懂可以随时在评论区评论或者私信我,我会帮助大家理解这个方法的底层到底是在做什么。这一定是你读过最全面,解析的最透彻的免费文章!希望满意的小伙伴可以评论区三连支持下~~。大家后续还有什么想看我分享的源码都可以和我说。
相关文章:

LinkedList接口源码解读
LinkedList 接口源码解读 前言 因为追求质量,所以写的较慢。大概在接下来的三天内会把LinkedList源码解析出完。已经出完啦!废话不多说,正片开始! (文章最后面有后记哦~) 大家都知道,LinkedL…...
nohup将代码放到后端运行查看nohup命令
tail -f nohup.outnohup python your_script.py > /path/to/your/directory/output.log 2>&1 &...
MacOS的100个超实用技巧
目录 1. 界面和导航 1.1 使用热角 1.2 多桌面切换 1.3 快速访问应用 1.4 隐藏/显示菜单栏 1.5 使用Mission Control 2. 文件管理 2.1 使用Finder标签 2.2 快速查看文件 2.3 标签式窗口管理 2.4 使用Smart Folders 2.5 文件重命名 3. 系统设置 3.1 自定义Dock 3.…...

本地调试指引文档
在开发组件库时,我们经常需要在真实的项目中测试组件库的功能,所以需要进行本地调试,本文介绍两种组件库本地调试流程, 1.使用beta版本 2.使用npm link 两种都可以作为本地调试的方案,本文作为一个参考资料࿰…...

【C++】一堆数组 冒泡排序
冒泡排序,一种很常见的排序法师 这章要划重点,很重要!! 排序思路为前一个元素与后一个元素比大小,一直循环一轮,找出最大/最小的那个元素后,进行下一轮,找到第二大/小的元素......…...
[最短路SPFA]--启动!!!!!
基础模板 #include<bits/stdc.h> #define ll long long #define fi first #define se second #define pb push_back #define PII pair<int,int > #define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) using namespace std; const int N 1e610; int …...

大模型是否潜在地进行多跳推理?
人工智能咨询培训老师叶梓 转载标明出处 以往的研究表明,基于Transformer的LLMs能够在参数中存储和检索事实信息,以完成简单提示,例如“Stevie Wonder的母亲是谁”。此外,当必要信息明确给出时,LLMs表现出了显著的上下…...
人为什么不能长期待在家里?三个原因告诉你答案
在现代社会的快节奏生活中,人们时常渴望能够拥有一段长时间待在家里的闲暇时光,幻想这会是一段惬意、舒适且自由的经历。然而,实际情况往往并非如此。许多人在经历了数日甚至更长时间的居家生活后,会逐渐感受到诸多负面情绪和不良影响。以下将详细阐述人为什么不能长期待在…...

MATLAB画散点密度图(附代码和测试数据的压缩包)
1. 有关 Matlab 获取代码关注WZZHHH回复关键词,或者咸鱼关注:WZZHHH123 怀俄明探空站数据解算PWV和Tm:怀俄明探空站数据解算PWV和Tm 怀俄明多线程下载探空站数据(包括检查和下载遗漏数据的代码):怀俄明多线…...
SSH配置命令
前置环境:端口配置IP地址,client和server之间可ping通,此处省略 server端: 开启stelnet [Huawei]stelnet server enable Info: Succeeded in starting the Stelnet server. aaa模式相关配置 #进入aaa模式 [Huawei]aaa # 添加用户admin和…...

谷粒商城实战记录-虚拟机开启密码认证登录
文章目录 一,虚拟机无法用用户名密码登录二,解决方案1,修改配置2,重启sshd服务3,测试SSH登录注意事项结论 参考文献 一,虚拟机无法用用户名密码登录 当使用Vagrant创建和管理虚拟机时,通常会通…...

C语言程序设计-[1] 基础语法
1、字符集 字符集:是ASCII字符集的一个子集。 注:基本上就是电脑键盘可以输入的一些字符。 2、标识符 标识符:用来命名程序中的一些实体,如:变量、常量、函数、数组名、类型名、文件名等。由一个或多个字符组成。 —…...
JavaSE第11篇:设计模式
一、创建型模式 1、工厂方法模式 2、抽象工厂模式 3、单例模式singleton /*** 单例* 饿汉式(线程安全的):在加载类的时候就会创建类的单例,并保存在类中。* 1.定义类变量实例并直接实例化,在类加载的时候就完成了实例化并保存在类中;* 2.定义无参构造…...

【Unity Shader】切线空间下计算凹凸映射
// Upgrade NOTE: replaced mul(UNITY_MATRIX_MVP,*) with UnityObjectToClipPos(*)Shader "Unlit/NormalTangent" {Properties{_Color("Color Tint", Color) (1, 1, 1, 1)_MainTex("Main Tex", 2D) "While"{}//法线纹理_BumpMap(&q…...

解决Ubuntu/Kali手动创建的启动器在dock上没有图标,且不能“添加到dock中“的问题
文章目录 问题描述问题解决解决方案 1 | 添加StartupWMClass字段解决方案 2 | 重命名文件名 如何获取 WM 值?方式 1 | xprop 命令方式 2 | 直接查看 问题描述 这个启动器无论是在菜单还是桌面都是正常的,只有在dock中没有图标,且不像其他APP…...

【Android】数据持久化——数据存储
持久化技术简介 在你打开完成了一份PPT之后关闭程序,再次打开肯定是希望之前的内容还存在在电脑上,一打开PPT,之前的内容就自动出现了。数据持久化就是将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下…...

如何通过谷歌外链快速增加网站流量?
利用谷歌外链提升流量的方法非常直接,但实际上,外链影响的是关键词排名,关键词排名提升了,自然就会有流量,所以谷歌外链不是直接能提升网站流量,而是间接的,下面,我会详细介绍几种有…...

vLLMcuda安装笔记
1. 引言 最近在部署Qwen模型时,文档上有提到强烈建议用vLLM来部署模型,按照公开的性能测试数据,用vLLM部署Qwen模型的文本推理速度要比transformers部署快3~4倍。带着这个好奇就开始安装尝试,但试下来这个安装过程并没有那么顺利…...

C++入门基本语法(2)
一、引用 1、基本概念与定义 引用不是新定义一个变量,而是给已存在的变量起一个别名,编译器不会为引用变量开辟内存空间,它和它所引用的变量公用同一块内存空间; 引用的写法:变量类型& 引用别名 变量ÿ…...

Internet Download Manager(IDM)2024中文版本有哪些新功能?6.42版本功能介绍
1. Internet Download Manager(IDM)是一款功能强大的下载管理器,支持所有流行的浏览器,并可提升下载速度高达5倍。 2. IDM具有智能下载逻辑加速器,可以设置文件下载优先级、分块下载等,提高下载效率。 IDM…...

业务系统对接大模型的基础方案:架构设计与关键步骤
业务系统对接大模型:架构设计与关键步骤 在当今数字化转型的浪潮中,大语言模型(LLM)已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中,不仅可以优化用户体验,还能为业务决策提供…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析
今天聊的内容,我认为是AI开发里面非常重要的内容。它在AI开发里无处不在,当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗",或者让翻译模型 "将这段合同翻译成商务日语" 时,输入的这句话就是 Prompt。…...
DeepSeek 赋能智慧能源:微电网优化调度的智能革新路径
目录 一、智慧能源微电网优化调度概述1.1 智慧能源微电网概念1.2 优化调度的重要性1.3 目前面临的挑战 二、DeepSeek 技术探秘2.1 DeepSeek 技术原理2.2 DeepSeek 独特优势2.3 DeepSeek 在 AI 领域地位 三、DeepSeek 在微电网优化调度中的应用剖析3.1 数据处理与分析3.2 预测与…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...
spring:实例工厂方法获取bean
spring处理使用静态工厂方法获取bean实例,也可以通过实例工厂方法获取bean实例。 实例工厂方法步骤如下: 定义实例工厂类(Java代码),定义实例工厂(xml),定义调用实例工厂ÿ…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...

如何理解 IP 数据报中的 TTL?
目录 前言理解 前言 面试灵魂一问:说说对 IP 数据报中 TTL 的理解?我们都知道,IP 数据报由首部和数据两部分组成,首部又分为两部分:固定部分和可变部分,共占 20 字节,而即将讨论的 TTL 就位于首…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...