返回顶部
首页 > 资讯 > 后端开发 > Python >以武侠形式理解Java LinkedList源码
  • 355
分享到

以武侠形式理解Java LinkedList源码

2024-04-02 19:04:59 355人浏览 泡泡鱼

Python 官方文档:入门教程 => 点击学习

摘要

目录一、LinkedList 的剖白二、LinkedList 的内功心法三、LinkedList 的招式1)招式一:增2)招式二:删3)招式三:改4)招式四:查四、LinkedLis

一、LinkedList 的剖白

大家好,我是 LinkedList,和 ArrayList 是同门师兄弟,但我俩练的内功却完全不同。师兄练的是动态数组,我练的是链表

问大家一个问题,知道我为什么要练链表这门内功吗?

举个例子来讲吧,假如你们手头要管理一推票据,可能有一张,也可能有一亿张。

该怎么办呢?

申请一个 10G 的大数组等着?那万一票据只有 100 张呢?

申请一个默认大小的数组,随着数据量的增大扩容?要知道扩容是需要重新复制数组的,很耗时间。

关键是,数组还有一个弊端就是,假如现在有 500 万张票据,现在要从中间删除一个票据,就需要把 250 万张票据往前移动一格。

遇到这种情况的时候,我师兄几乎情绪崩溃,难受的要命。师父不忍心看到师兄这样痛苦,于是打我进入师门那一天,就强迫我练链表这门内功,一开始我很不理解,害怕师父偏心,不把师门最厉害的内功教我。

直到有一天,我亲眼目睹师兄差点因为移动数据而走火入魔,我才明白师父的良苦用心。从此以后,我苦练“链表”这门内功,取得了显著的进步,师父和师兄都夸我有天赋。

链表这门内功大致分为三个层次:

  • 第一层叫做“单向链表”,我只有一个后指针,指向下一个数据;
  • 第二层叫做“双向链表”,我有两个指针,后指针指向下一个数据,前指针指向上一个数据。
  • 第三层叫做“二叉树”,把后指针去掉,换成左右指针。

但我现在的功力还达不到第三层,不过师父说我有这个潜力,练成神功是早晚的事。

先赞后看:《Java 程序员进阶之路》专栏在 GitHub 上已经开源了,有 gitHub 账号的小伙伴,来安排一波 star 呀!看能不能冲一波 trending 榜单,求求各位了。

GitHub 地址:https://github.com/itwanger/toBeBetterJavaer

二、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;
    }
}

它由三部分组成:

  • 节点上的元素
  • 下一个节点
  • 上一个节点

我画幅图给你们展示下吧。

  • 对于第一个节点来说,prev 为 null;
  • 对于最后一个节点来说,next 为 null;
  • 其余的节点呢,prev 指向前一个,next 指向后一个。

我的内功心法就这么简单,其实我早已经牢记在心了。但师父叮嘱我,每天早上醒来的时候,每天晚上睡觉的时候,一定要默默地背诵一遍。虽然我有些厌烦,但我对师父的教诲从来都是言听计从。

三、LinkedList 的招式

和师兄 ArrayList 一样,我的招式也无外乎“增删改查”这 4 种。在此之前,我们都必须得初始化。


LinkedList<String> list = new LinkedList();

师兄在初始化的时候,默认大小为 10,也可以指定大小,依据要存储的元素数量来。我就不需要。

1)招式一:增

可以调用 add 方法添加元素:


list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");

add 方法内部其实调用的是 linkLast 方法:


public boolean add(E e) {
    linkLast(e);
    return true;
}

linkLast,顾名思义,就是在链表的尾部链接:


void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}
  • 添加第一个元素的时候,first 和 last 都为 null。
  • 然后新建一个节点 newNode,它的 prev 和 next 也为 null。
  • 然后把 last 和 first 都赋值为 newNode。

此时还不能称之为链表,因为前后节点都是断裂的。

  • 添加第二个元素的时候,first 和 last 都指向的是第一个节点。
  • 然后新建一个节点 newNode,它的 prev 指向的是第一个节点,next 为 null。
  • 然后把第一个节点的 next 赋值为 newNode。

此时的链表还不完整。

  • 添加第三个元素的时候,first 指向的是第一个节点,last 指向的是最后一个节点。
  • 然后新建一个节点 newNode,它的 prev 指向的是第二个节点,next 为 null。
  • 然后把第二个节点的 next 赋值为 newNode。

此时的链表已经完整了。

我这个增的招式,还可以演化成另外两个:

  • addFirst() 方法将元素添加到第一位;
  • addLast() 方法将元素添加到末尾。

addFirst 内部其实调用的是 linkFirst:


public void addFirst(E e) {
    linkFirst(e);
}

linkFirst 负责把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。


private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

addLast 的内核其实和 addFirst 差不多,就交给大家自行理解了。

2)招式二:删

我这个删的招式还挺多的:

  • remove():删除第一个节点
  • remove(int):删除指定位置的节点
  • remove(Object):删除指定元素的节点
  • removeFirst():删除第一个节点
  • removeLast():删除最后一个节点

remove 内部调用的是 removeFirst,所以这两个招式的功效一样。

remove(int) 内部其实调用的是 unlink 方法。


public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。


E unlink(Node<E> x) {
    // assert x != null;
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }

    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }

    x.item = null;
    size--;
    modCount++;
    return element;
}

remove(Object) 内部也调用了 unlink 方法,只不过在此之前要先找到元素所在的节点:


public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

这内部就分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。equals 是不能用来判 null 的,会抛出 NPE 错误。

removeFirst 内部调用的是 unlinkFirst 方法:


public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

unlinkFirst 负责的就是把第一个节点毁尸灭迹,并且捎带把后一个节点的 prev 设为 null。


private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

3)招式三:改

可以调用 set() 方法来更新元素:


list.set(0, "沉默王五");

来看一下 set() 方法:


public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

首先对指定的下标进行检查,看是否越界;然后根据下标查找原有的节点:


Node<E> node(int index) {
    // assert isElementIndex(index);

    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

size >> 1:也就是右移一位,相当于除以 2。对于计算机来说,移位比除法运算效率更高,因为数据在计算机内部都是二进制存储的。

换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历。

找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动。

4)招式四:查

我这个查的招式可以分为两种:

  • indexOf(Object):查找某个元素所在的位置
  • get(int):查找某个位置上的元素

indexOf 的内部分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。因为 equals 是不能用来判 null 的,会抛出 NPE 错误。


public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

get 方法的内核其实还是 node 方法,这个之前已经说明过了,这里略过。


public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

其实,查这个招式还可以演化为其他的一些,比如说:

  • getFirst() 方法用于获取第一个元素;
  • getLast() 方法用于获取最后一个元素;
  • poll()pollFirst() 方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的);
  • pollLast() 方法用于删除并返回最后一个元素;
  • peekFirst() 方法用于返回但不删除第一个元素。

四、LinkedList 的挑战

说句实在话,我不是很喜欢和师兄 ArrayList 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。

虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。

比如说,我们俩在增删改查时候的时间复杂度。

也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。

无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。

好了,LinkedList 这篇就到这了。

如果大家有闲情逸致的话,建议手撕一下链表,可以从单向链表开始撕起。

希望大家能点赞下,给我注入一点点更新的动力。我也会不断地提升品质,给大家带来更硬核的技术文章,笔芯~

到此这篇关于以武侠形式理解Java LinkedList源码的文章就介绍到这了,更多相关Java LinkedList内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: 以武侠形式理解Java LinkedList源码

本文链接: https://lsjlt.com/news/156482.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • 以武侠形式理解Java LinkedList源码
    目录一、LinkedList 的剖白二、LinkedList 的内功心法三、LinkedList 的招式1)招式一:增2)招式二:删3)招式三:改4)招式四:查四、LinkedLis...
    99+
    2024-04-02
  • 如何以武侠形式理解Java LinkedList源码
    如何以武侠形式理解Java LinkedList源码,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。一、LinkedList 的剖白大家好,我是 LinkedLi...
    99+
    2023-06-25
  • 【JAVA】CyclicBarrier源码解析以及示例
    文章目录 前言CyclicBarrier源码解析以及示例主要成员变量核心方法 应用场景任务分解与合并应用示例 并行计算应用示例 游戏开发应用示例输出结果 数据加载应用示例 并发工具的协...
    99+
    2023-12-22
    java 开发语言 jvm
  • Java Lambda 表达式源码解析
    Java Lambda 源码分析 问题: Lambda 表达式是什么?JVM 内部究竟是如何实现 Lambda 表达式的?为什么要这样实现? 一、基本概念 1、Lambda 表达式 下面的例子中,() -> System.out.p...
    99+
    2021-12-25
    Java Lambda 表达式 Java Lambda源码
  • 如何理解Java SynDemo对象源代码
    本篇文章为大家展示了如何理解Java SynDemo对象源代码,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。Java SynDemo对象一直在我们的语言使用中使用,其实在不断的学习中我们还是在源代码...
    99+
    2023-06-17
  • Java基础之如何理解Object源码
    本篇内容主要讲解“Java基础之如何理解Object源码”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Java基础之如何理解Object源码”吧!getClasspublic fina...
    99+
    2023-06-15
  • 如何理解Redisson分布式锁的源码
    本篇内容介绍了“如何理解Redisson分布式锁的源码”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!Red...
    99+
    2024-04-02
  • 【Java】智慧工地管理系统源码(SaaS模式)
    智慧工地是聚焦工程施工现场,紧紧围绕人、机、料、法、环等关键要素,综合运用物联网、云计算、大数据、移动计算和智能设备等软硬件信息技术,与施工生产过程相融合。    一、什么是智慧工地 智慧工地是指利用移动互联、物联网、智能算法、地理信...
    99+
    2023-08-30
    java 智慧工地 微服务架构 spring cloud SaaS模式 工程施工 智慧工地云平台
  • Vue解读之响应式原理源码剖析
    目录初始化initState()initProps()initData()observe()ObserverdefineReactive()依赖收集DepWatcher依赖收集过程移...
    99+
    2024-04-02
  • MyBatis源码剖析之Mapper代理方式详解
    目录源码剖析-getmapper()源码剖析-invoke()具体代码如下: //前三步都相同 InputStream resourceAsStream = Resources.ge...
    99+
    2024-04-02
  • Java源码难点突破Lambda表达式执行原理
    目录引导语1、Demo2、异常判断法3、javap命令法4、总结引导语 大家都知道 Java8 中新增了 Lambda 表达式,使用 Lambda 表达式可以对代码进行大量的优化,用...
    99+
    2024-04-02
  • Java详解HashMap实现原理和源码分析
    目录学习要点:1、什么是HashMap?2、HashMap的特性3、HashMap的数据结构4、HashMap初始化操作4.1、成员变量4.2、 构造方法5、Jdk8中HashMap...
    99+
    2024-04-02
  • Java Spring AOP源码解析之事务实现原理
    目录不用Spring管理事务?编程式事务管理使用PlatformTransactionManager使用TransactionTemplate声明式事务管理使用@Transactio...
    99+
    2024-04-02
  • 如何理解Java容器中Map的源码分析
    本篇文章为大家展示了如何理解Java容器中Map的源码分析,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。如果没有特别说明,以下源码分析基于 JDK 1.8。一、HashMap为了便于理解,以下源码分...
    99+
    2023-06-05
  • 如何理解Java容器中ArrayList的源码分析
    这篇文章给大家介绍如何理解Java容器中List的源码分析,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。如果没有特别说明,以下源码分析基于 JDK 1.8。一、ArrayList1. 概览实现了 RandomAcces...
    99+
    2023-06-05
  • OneFlow源码解析之Eager模式下Tensor存储管理
    目录1不同Tensor类型的存储管理方式2Tensor 存储相关类的关系3通过虚拟机指令为 Tensor 分配存储3.1 存储相关对象的构造3.2 在指令执行过程中分配 tensor...
    99+
    2023-05-16
    OneFlow Eager Tensor Tensor存储管理
  • axios拦截器工作方式及原理源码解析
    目录axios 拦截器的配置方式use() 方法的定义拦截器如何执行拦截器回调方法的添加顺序同步执行请求拦截器(顺序执行)异步执行请求拦截器(同时执行)Q&A拦截器是如何工作...
    99+
    2023-02-10
    axios拦截器工作原理 axios 拦截器
  • 深度源码解析Java 线程池的实现原理
    目录线程池的优点线程池的实现原理ThreadPoolExecutor阻塞队列线程池工厂拒绝策略提交任务到线程池execute 方法submit 方法关闭线程池合理的参数7、本文小结j...
    99+
    2024-04-02
  • Java SpringBoot自动装配原理详解及源码注释
    目录一、pom.xml文件1.父依赖2.启动器:二、主程序:剖析源码注解:三、结论:一、pom.xml文件 1.父依赖 主要是依赖一个父项目,管理项目的资源过滤以及插件! 资源过滤...
    99+
    2024-04-02
  • JAVA 枚举单例模式及源码分析的实例详解
    JAVA 枚举单例模式及源码分析的实例详解      单例模式的实现有很多种,网上也分析了如今实现单利模式最好用枚举,好处不外乎三点:线程安全不会因为序列化而产生新实例防止反射攻击但是貌似没...
    99+
    2023-05-31
    java 枚举 单例
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作