LinkedList接口源码解读

news2024/11/15 21:49:50

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类实现了ListDequeCloneableSerializable 接口。

  • List : **表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。 **

  • Deque继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。

  • CloneableCloneable 注解是一个标记接口,我们点进去会发现它并没有任何方法。此接口表明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关键字。

  • transientJava语言的关键字,用来表示一个成员变量不是该对象序列化的一部分。当一个对象被序列化的时候,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 是否为 null
            Node 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 -> 3
                    pred.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 接口的很多方法,所以我们有很多种方式插入元素。

下面我将以 ListQueueStack这三种数据结构分开带大家阅读源码。

可能会有朋友好奇,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
     */
    @Override
    public boolean push(E value) {
        if (isFull()) {
            return false;
        }
        head.next = new Node<E>(value, head.next);
        size++;
        return true;
    }

    /**
     * 从栈顶弹出元素
     *
     * @return 栈非空返回栈顶元素,栈为空返回 null
     */
    @Override
    public E pop() {
        if (isEmpty()) {
            return null;
        }
        E value = head.next.value;
        head.next = head.next.next;
        size--;
        return value;
    }

    /**
     * 返回栈顶元素,不弹出
     *
     * @return 栈非空返回栈顶元素,栈为空返回 null
     */
    @Override
    public E peek() {
        if (isEmpty()) {
            return null;
        }
        return head.next.value;
    }

    /**
     * 检查栈是否为空
     *
     * @return  空返回 true,否则返回 false
     */
    @Override
    public boolean isEmpty() {
        return head.next == null;
    }

    /**
     * 检查栈是否已满
     *
     * @return 满了返回 true,否则返回 false
     */
    @Override
    public boolean isFull() {
        return capacity == size;
    }

    /**
    * 方便我用 增强for循环 测试
    */
    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            Node<E> p = head.next;

            @Override
            public boolean hasNext() {
                return p != null;
            }

            @Override
            public 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)。

源码:

// 在 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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1975745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++标准模版库】模拟实现vector+迭代器失效问题

模拟实现vector 一.vector成员变量二.构造函数1.无参&#xff08;默认&#xff09;构造2.有参构造3.拷贝构造1.传统写法2.现代写法 三.vector对象的容量操作1.size2.capacity3.clear4.empty5.reserve6.resize 四.vector对象的访问及遍历操作1.operator[]2.实现迭代器&#xff1…

一次Linux端口冲突问题排查记录

更多技术博客&#xff0c;请关注微信公众号&#xff1a;运维之美 一、问题现象 实施同学反馈说我们的服务部署到客户环境后&#xff0c;运行正常&#xff0c;但是后台页面就是无法打开&#xff0c;出现访问白屏问题 从console报错看是其中一个接口响应存在问题&#xff0c;…

老兵永远跟党走,团结奋进新征程-百余老兵庆八一暨《永远闪光的军徽》新书发布座谈会在昆举行

7月30日上午,由中国红色文化研究会老山精神专业委员会主办,昆明乡羊香园承办的百余老兵庆祝“八一”建军节暨《永远闪光的军徽》新书发布座谈会在昆明滇池湖畔乡羊香园举行。 上午10时左右,老战士、曾担任云南省军区领导的黄光汉、陶昌廉、和志光、李继才、雷玉德、王永银老将军…

海思Hi35XX系列(一)环境搭建与挂载

小白一个&#xff0c;新的开发板刚到手有点懵&#xff0c;之前没弄过没有经验&#xff0c;简单记录一下吧 一般买开发板都会给带一个已经配置好的虚拟机文件&#xff0c;直接使用就可以 一、下载安装虚拟机与镜像文件 VMware-workstation16.1.0 我的镜像文件是官方文档资料…

亨廷顿小勇士必看!你的维生素补给站来啦~

Hey小伙伴们~&#x1f44b; 今天咱们来聊聊一个超勇敢的话题——亨廷顿舞蹈症宝贝们的健康小秘密&#xff01;&#x1f389; 虽然路途偶尔有点小颠簸&#xff0c;但记得哦&#xff0c;你们是最闪耀的星星✨&#xff01;让我们一起加油&#xff0c;用对维生素&#xff0c;守护这…

C#知识|文件与目录操作:对象的创建、保存、读取

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 面向对象编程的特点就是一切皆对象&#xff0c;操作的也是对象&#xff0c;本节学习文件与目录操作中&#xff0c;对象的保存&#xff1b; 以下为学习笔记。 01 对象的特点 ①&#xff1a;对象运行在内存中&#xff…

4.5K Stars!为 RAG 而生的数据工程神器

—1 大模型 RAG 的难题是什么&#xff1f; RAG 或者 Fine-tuning 微调作为大模型的增强技术&#xff0c;最核心的技术在于如何把企业的私有数据清洗转换成知识&#xff0c;企业中能够第一时间拿到的私有数据&#xff0c;往往是异构的、数据质量参差不齐&#xff0c;通过数据…

ICM-20948芯片详解(3)

接前一篇文章&#xff1a;ICM-20948芯片详解&#xff08;2&#xff09; 三、引脚图、信号描述及内部框图 1. 引脚图 ICM-20948的引脚图如下图所示&#xff1a; 2. 引脚详细描述 ICM-20948的引脚详细描述如下表所示&#xff1a; 注意&#xff1a;不支持在SCL/SCLK和nCS引脚保…

Linux系统窗口水印难点分析

给应用程序加水印是保护数据的一种方式&#xff0c;window上可以通过给进程通过注入的方法给进程的窗口创建一个同大小的副窗口&#xff0c;在副窗口上绘制水印内容&#xff0c;同时设置副窗口透明同时透传事件&#xff0c;这样就可以达到在源窗口上显示水印的效果且不影响程序…

易媒助手:神似融媒宝的自媒体运营工具,新人送7天中级VIP

自媒体运营工具中还有一个易媒助手&#xff0c;功能与融媒宝、蚁小二类似&#xff0c;免费用户可发5个账号&#xff0c;三者同时用就可发15个账号了&#xff0c;所以今天也给大家介绍下&#xff1a; 易媒助手简介 易媒助手于2017年开发&#xff0c;致力于成为中国更优秀的新媒…

springboot集成海康SDK,设备抓图,热成像仪设置多个点代码获取,以及针对红外图点击某一点获取该点温度的需求

本文会介绍java对海康sdk的三个功能&#xff1a; 1、用代码实时抓图 2、用代码获取热成像仪21个点的坐标及其实时温度 3、针对海康热成像仪抓取的热图能够随便点击任意一个点就能获取其温度的功能。 第一个功能&#xff0c;抓图 抓图 在海康提供的sdk中取流后抓图调用的是 …

ollama运行阿里云通义千问72B大模型

准备 安装ollama https://github.com/ollama/ollama 模型 合并gguf copy /B qwen2-72b-instruct-q5_k_m-00001-of-00002.gguf qwen2-72b-instruct-q5_k_m-00002-of-00002.gguf qwen2-72b-instruct-q5_k_m.gguf设置并启动 新建Modelfile FROM ./qwen2-72b-instruct-q5_k…

【课程总结】Day18:Seq2Seq的深入了解

前言 在上一章【课程总结】Day17&#xff08;下&#xff09;&#xff1a;初始Seq2Seq模型中&#xff0c;我们初步了解了Seq2Seq模型的基本情况及代码运行效果&#xff0c;本章内容将深入了解Seq2Seq模型的代码&#xff0c;梳理代码的框架图、各部分组成部分以及运行流程。 框…

想做linux内核开发,该怎么开始(上)

作为一名应届生在选择从事 Linux 内核开发这一职业领域时&#xff0c;需要系统地规划自己的职业道路&#xff0c;这将有助于你更准确地了解未来的发展方向并制定相应的学习和职业发展计划。在这篇文章中&#xff0c;我将向你介绍应届生在 Linux 内核开发领域的职业道路规划&…

O’Reilly

--江上往来人&#xff0c;但爱鲈鱼美。 --君看一叶舟&#xff0c;出没风波里。 OReilly OReilly出版社出版的技术类图书 俗称动物系列 应该是每个技术人员的必备手册。 OReilly动物系列&#xff08;中译本&#xff09; 简介" 动物系列作为 OReilly 书籍的典型代表被普遍…

【Apache Doris】周FAQ集锦:第 18 期

【Apache Doris】周FAQ集锦&#xff1a;第 18 期 SQL问题数据操作问题运维常见问题其它问题关于社区 欢迎查阅本周的 Apache Doris 社区 FAQ 栏目&#xff01; 在这个栏目中&#xff0c;每周将筛选社区反馈的热门问题和话题&#xff0c;重点回答并进行深入探讨。旨在为广大用户…

基于级联深度学习算法在双参数MRI中检测前列腺病变的评估| 文献速递-AI辅助的放射影像疾病诊断

Title 题目 Evaluation of a Cascaded Deep Learning–based Algorithm for Prostate Lesion Detection at Biparametric MRI 基于级联深度学习算法在双参数MRI中检测前列腺病变的评估 Background 背景 Multiparametric MRI (mpMRI) improves prostate cancer (PCa) dete…

如何对我们要多次使用的页面进行一个抽取

有的时候,一个页面我们要多次使用,该怎么抽取呢? 创建一个文件夹,用于存放多次使用的页面 将要多次使用的组件(<template>)和风格(<style>)剪切出来,放入新建的页面 直接进行引用 导入 然后就可以使用

【FPGA设计】Vitis AI概述

一. Vitis AI简介 Vitis AI 是由 Xilinx&#xff08;现已被 AMD 收购&#xff09;提供的一套工具链和软件开发平台&#xff0c;用于简化和加速在基于 Xilinx FPGA 或自适应计算加速平台 (ACAP) 上部署深度学习推理应用的过程。Vitis AI 的目标是让开发者能够更容易地利用 FPGA…

python-素数回文数的个数(赛氪OJ)

[题目描述] 求 11 到 n 之间&#xff08;包括 n&#xff09;&#xff0c;既是素数又是回文数的整数有多少个。输入&#xff1a; 一个大于 11 小于 10000 的整数 n。输出&#xff1a; 11 到 n 之间的素数回文数个数。样例输入1 23 样例输出1 1 提示&#xff1a; 回文数指左右对…