【源码学习】深入剖析核心源码之 LinkedList

看源码是必不可少的一步,源码的学习通常是将常用的方法的实现理解透彻并能合理的使用,掌握其特性。而输出才能更好的输入,所以写下博客将自己的学习记录下来,对于Java需要掌握的就是各种容器,我们已经上次学习了ArrayList的源码,这次继续来学习LinkedList的源码。

一、LinkedList基础知识

  1. 节点的定义:
    Node类是LinkedList中的私有内部类,决定着LinkedList中元素的形式。
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;
        }
}

Java中实现的是双向链表,每一个节点既有前驱节点引用prev指向该节点的前一个节点,也有后继节点引用next指向该节点的下一个节点。

  1. LinkedList底层成员变量
    (1)size表示该链表中节点的个数
    (2)Node first表示链表中的第一个节点
    (3)Node last表示链表中最后一个节点。
	transient int size = 0;

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;

    /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

对于size成员变量,还为其封装了一个成员方法用于获取链表的节点个数
在这里插入图片描述

一、常用方法实现

在这里插入图片描述

1.LinkedList()

LinkedList提供了一个空的构造方法,对于ArrayList有一个初始化底层数组大小的构造器,而LinkedList没有,这是因为ArrayList底层是基于数组来实现的,在构造时可以传入底层数组的长度,注意!这里的长度指的是底层数组的长度而不是list的初始长度初始时List是没有元素的,长度也是0。而LinkedList是使用链表结构把一个个节点元素都串接起来,长度是动态变化的,因此也不需要初始化容量。
在这里插入图片描述

2. addFirst(E e)

在Java中实现addFirst(E e)函数时通过在函数中调用私有的成员方法linkFirst()来实现的,接下来主要就是分析该私有函数。
在这里插入图片描述
实现元素在链表中的头插。由于LinkedList的实现是双引用,所以在插入一个节点时要考虑到prvnext的改变,以及成员变量firstlast的改变。
当插入新节点后,首先需要创建节点并初始化节点的前驱和后继节点,接着使得first节点指向newNode,然后就需要根据first引用的指向判断两种情况:

  • 插入之前链表为空(first == null):
    在这里插入图片描述
    开始时firstlast均为null,当改变first节点指向后,此时由于last开始时也为null,因此需要修改last引用的指向,使之同样指向newNode

  • 插入之前链表中已有元素(first != null):
    在这里插入图片描述
    如果插入前已有元素,那么同前一种情况不一样的就是:由于last引用不受影响因此不需要修改其指向,反而需要使得插入节点的后继节点的prv引用指向该插入节点。

而为了能进行两种情况在第三步的判断,就需要首先将未插入节点前的first引用保存下来而且使用final关键字修饰,使得该引用不会随着后面代码对其修改而修改。
在这里插入图片描述

3.addLast(E e)

同样,实现addLast(E e)函数也是调用类私有方法linkLast(E e)实现,接下来开始分析该私有函数。
在这里插入图片描述
实现元素在链表中的尾插。与头插思想一样,理解了上面的头插,尾插的代码也很好理解了。不过对于尾插,判断的依据就成了last引用。同样两种情况:

  • 插入之前链表为空(last == null)
    当插入之前链表为空时,开始时firstlast均为null,插入节点后,就应当改变last节点指向。又因为此时first开始时也为null,因此需要修改first引用的指向,使之同样指向newNode

  • 插入之前链表中已有元素(last != null)
    如果插入前已有元素,那么同前一种情况不一样的就是:由于first引用不受影响因此不需要修改其指向,反而需要使得插入节点的前驱节点的next引用指向该插入节点。

而为了能进行两种情况在第三步的判断,就需要首先将未插入节点前的last引用保存下来而且使用final关键字修饰,使得该引用不会随着后面代码对其修改而修改。
在这里插入图片描述
总体而言只要理解好了头插那么尾插也自然理解了。

分析源码我们发现,add(E e) 方法也是通过尾插来实现的。
在这里插入图片描述

4.removeFirst()

实现一个链表的头删功能。在解析源码时,我们发现在该方法中检验了first是否为空,如果非空,则在该方法中又调用了类私有方法unlinkFirst来进行头删。
在这里插入图片描述
那么对于unlinkFirst方法,事实上就是进行了头删操作。首先将第一个节点的成员域都置为空以便垃圾回收器回收,为了修改first引用指向该节点的后继节点就需要提前保存其后继节点。最后根据该节点的后继节点是否为空的仍然分为两种情况:

  • 删掉该节点后,链表为空(first.next == null)
    由于删掉该节点后链表为空,也即链表中的firstlast节点均指向要删除的节点。所以即使是头删也会影响到last节点,需要将last节点置空。
  • 删掉该节点后,链表中还有节点(first.next != null)
    由于链表中不只有一个节点,也即链表头删也不会影响到last节点,此时需要将要删除元素的后继节点的prev引用置空。

在这里插入图片描述

5.removeLast()

实现链表尾删功能。同样,在该函数中也是判断尾节点是否为空,接着调用私有成员函数unlinkLast进行尾删。
在这里插入图片描述
同样,思路和头删相似。将尾节点的成员域都置空,尾删会影响last引用,所以将last引用改为该节点的前驱节点。此时根据前驱结点是否为空又有两种情况:

  • 删掉该节点后,链表为空(last.prev == null)
    由于删掉该节点后链表为空可知,在未删除之前,链表的firstlast均指向该节点,当删除后,不仅应该修改last节点,也应该将first节点也置为空。
  • 删掉该节点后,链表中还有其他节点(last.prev != null)
    在此种情况下,删除该节点后不会影响first引用的指向。所以此时将last修改指向为该节点的前驱结点,同时也需要将该节点的前驱结点的next成员域置空。

这两种情况均需要提前保存当前节点的前驱结点,这样在接下来的修改中不会收到影响。
在这里插入图片描述

6.contains(Object o)

该方法可判断链表中是否存在对象o。contains(Object o)方法内部也是再次调用成员函数indexOf(Object o) 而该函数的作用就是返回该对象在链表中第一次出现的索引。而contains(Object o)方法通过判断其返回值来实现其功能。
在这里插入图片描述
对于indexOf(Object o)方法,其实现思想也不复杂,就是将链表遍历一遍与对象o作比较。这里的判断语句用力啊判断对象o是否为空。若为空则可以通过==操作符判断相等,否则就需要调用equals方法进行判断相等,至于二者的不同,可以参考文章——>hashCode,equals,== 三者的区别是什么?
在这里插入图片描述

7.clear()

通过该方法可以将链表中的所有节点都清空。
实现思想也不复杂,还是遍历整个链表并将每一个链表的成员域都置空。最后将firstlast也置空。
在这里插入图片描述

8.get(int index)

得到index下标位置的节点。在该方法中又调用了node(int index)方法。
在这里插入图片描述
通过node(int index)方法可以找到index位置的节点。这里源码通过了一种优化的方式,通过判断index在前半部分还是后半部分来决定是从前向后遍历还是从后向前遍历。这样就无需将整个链表都遍历。
在这里插入图片描述

9.set(int index, E element)

实现将index位置的节点的值修改为element。首先对下标进行合法性检验,合法后,就将旧的成员域替换为新的element
在这里插入图片描述

10.add(int index, E element)

在链表中的指定位置插入指定的元素。 首先对index进行合法性校验,接着进行优化,如果indexsize相等,则进行尾插,否则调用linkBefore(E e, Node<E> succ)进行插入。
在这里插入图片描述
linkBefore方法是在某一结点前插入一个新的节点。这里又涉及到两种情况:

  • succ节点就是头节点,插入就变成了头插(succ.prev == null)
    先将succ节点的前驱保存,接着将要插入的节点的prev域初始化为succ节点的前驱,next域初始化为succ节点。接着使succ节点的前驱结点指向该插入节点。如果成为头插的情况,那么就需要修改first引用使其指向插入的节点。
    未插入前:
    在这里插入图片描述
    插入后:
    在这里插入图片描述
  • succ节点还有前驱节点(succ.prev != null)
    先将succ节点的前驱保存,接着将要插入的节点的prev域初始化为succ节点的前驱,next域初始化为succ节点。接着使succ节点的前驱结点指向该插入节点。如果succ节点还有前驱节点,就要使succ的前驱结点的next域指向该插入节点。
    未插入前:
    在这里插入图片描述
    插入后:
    在这里插入图片描述

源代码:
在这里插入图片描述

其实链表的基础操作只要能完全理解一种那么剩下的也都很好理解了,如果对上面的方法实现还有不清楚的,可以试着自己画一下图,自己实现该方法就会学到很多。如果有任何问题欢迎小伙伴们指正,如果对你有帮助欢迎点赞关注,接下来一起学习进步。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值