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…...
深入理解 C 语言中的联合体
目录 引言 一、 联合体的定义与基本用法 1.联合体的定义 2.基本用法 二、 联合体与结构体的区别 1.结构体 2.联合体 3.对比 三、联合体的优势 1. 节省内存 2. 提高效率 3. 代码简洁性 四、联合体的存储细节 1.内存对齐 2.大小计算 五、联合体的高级用法 1.匿…...
OpenCV||超详细的几何变换
2D图像几何变换的33矩阵: 图像常见的几何变换: 图像来源:《OpenCV 4.5计算机视觉开发实战:基于Python》作者:朱文伟 李建英; 1. 平移(Translation) 在OpenCV中,平移不是…...
网络程序设计基础概述
文章目录 前言一、网络程序设计基础二、网络协议 1.IP协议2.TCP与UDP协议三、端口与套接字总结 前言 网络程序设计编写的是与其他计算机进行通信的程序代码。Java将网络程序所需要的东西封装成了不同的类。开发者只需要创建这些类的对象,调用相应的方法,…...
MySQL:数据库用户
数据库用户 在关系型数据库管理系统中,数据库用户(USER)是指具有特定权限和访问权限的登录账户。每个用户都有自己的用户名和密码,以便系统可以通过认证来识别他们的身份。数据库用户可以登录数据库,在其中执行各种类…...
用TensorFlow训练自己的第一个模型
现在学AI的一个优势就是:前人栽树后人乘凉,很多资料都已完善,而且有很多很棒的开源作品可以学习,感谢大佬们 项目 项目源码地址 视频教程地址 我在大佬的基础上基于此模型还加上了根据特征值缓存进行快速识别的方法,…...
MySQL数据库入门基础知识 【1】推荐
数据库就是储存和管理数据的仓库,对数据进行增删改查操作,其本质是一个软件。 首先数据有两种,一种是关系型数据库,另一种是非关系型数据库。 关系型数据库是以表的形式来存储数据,表和表之间可以有很多复杂的关系&a…...
Anaconda下的 jupyter notebook安装及使用
安装 打开Anaconda Powershell Prompt或Anconda Prompt 输入命令conda install jupyter notebook进行安装 启动 切换到工作目录,输入命令jupyter notebook等待浏览器打开网页 命令行启动jupyter notebook的链接复制到浏览器同样可以打开jupyter notebook 在Ancon…...
C语言初阶(11)
1.结构体定义 结构体就是一群数据类型的集合体。这些数据类型被称为成员变量。结构的成员可以是标量、数组、指针,甚至是其他结构体。 2.结构体的声明和结构体变量命名与初始化 结构体声明由以下结构组成 struct stu {char name[12];int age; }; 结构体命名有两…...
Unity获取Animator动画播放完成事件
整理了一些在日常经验中处理动画播放完成事件的方法 方法: 1.Dotween配合异步实现 2.状态机计时方法实现 3.原生动画行为方法实现 方法一:Dotween异步方法 using UnityEngine; using System.Threading.Tasks; using DG.Tweening;public class PlayerAnimAsync : M…...
git submodule 使用
在Git中,子模块(submodule)是一种将一个Git仓库作为另一个Git仓库的子目录嵌入的方式。这使得主仓库能够跟踪和管理对外部依赖的更改。 添加子模块 初始化父仓库:如果你还没有创建父仓库,先创建它。 添加子模块&…...
网站论坛怎么建设/进行seo网站建设
在《人月神话》中,布鲁克斯老先生将维护软件的" 概念完整性" 作为软件开发的核心问题。软件之所以很复杂、难以维护,根本原因就在于软件的概念完整性遭到了破坏,甚至开发团队的成员从来就没有意识到有必要去维护软件的概念完整性&a…...
wordpress信用卡支付宝/sem投放是什么意思
在 Spring.Net 中对象初始化的方式分为两种: ① 急切实例化,也就是说 Spring.Net 容器初始化的时候将对象先实例化出来。 ② 延迟实例化,也就是说我们在调用 GetObject 方法时才实例化该对象。 Spring.Net 默认使用的 急切实例化 ( lazy-init…...
汽配网站源码/网店如何引流与推广
git .git目录提交我正在撰写一篇文章,概述了如何编写良好的Git提交消息,以及开发人员应遵循的各种Git提交消息约定和规则。 但是,正如我写的开发人员应遵循的最佳做法,我不断地发现自己在哪些开发商不应该做一个内部讨论。 我希望…...
建设银行网站的目的是什么/关键词词库
数据结构中的栈不要与 Java 中的栈混淆,他们俩不是一回事,数据结构中的栈是一种受限制的线性表,栈具有先进后出、后进先出的特点,因为栈只允许访问最后一个数据项,即最后插入的数据项。也许你会有疑问,栈既…...
沈阳网站制作找网势科技/百度客服在线咨询
第一,谈谈final, finally, finalize的区别。第二,Anonymous Inner Class (匿名内部类) 是否可以extends(继承)其它类,是否可以implements(实现)interface(接口)?第三,Static Nested Class 和 Inner Class的不同,说得越…...
网站建设柒首先金手指2/竞价排名适合百度吗
第一次做性能测试,按照操作文档磕磕碰碰的完成了,并且拿到了结果,看到一堆的指标和数据,还是傻眼了,不知道各个指标是什么意思了。 咨询了大牛和度娘,消化理解了一下,不知道是不是正确的。 CPU使…...