LinkedList源码分析

LinkedList简介

上次咱们分析了ArrayList的源码,知道ArrayList查找元素的速度特别快,时间复杂度是O(1),但是如果不是在数组末尾插入元素或者插入时触发了扩容机制,这时侯的时间复杂度为O(n),今天我们来看另一个list类型的集合对象LinkedList,它对元素的插入的时间复杂度可以达到O(1),同样,我们先来看下LinkedList的继承关系图,如下所示:
在这里插入图片描述
可以看到LinkedList和ArrayList一样都实现了java.io.Serializable,Cloneable和List接口,因此它也拥有克隆,实现序列化等特性,提供了相关的添加、删除、修改、遍历等功能。
需要注意的是LinkedList继承了AbstractSequentialList,所以LinkedList只支持顺序访问,并不具备ArrayList随机访问的特性。接下来我们就准备开始分析LinkedList的相关源码。

LinkedList数据结构

在分析源码之前,先展示出LinkedList的底层数据结构长啥样,如下图所示:
在这里插入图片描述
LinkedList底层是一个双向链表的结构,每个节点包含两个引用,prev指向当前节点前一个节点,next指向当前节点后一个节点,可以从头结点遍历到尾结点,也可以从尾结点遍历到头结点。

LinkedList核心源码

LinkedList类字段参数

在这里插入图片描述

  • size:链表的元素个数。
  • first:链表的头节点引用。
  • last:链表的尾节点的引用。
    在这里插入图片描述
    Node是LinkedList的静态内部类,是链表组成的核心结构。
  • item:存放元素数据。
  • next:指向下一个节点的地址引用。
  • prev:指向前一个节点的地址引用。

LinkedList构造方法

LinkedList有两个构造方法,一个是无参构造,没有任何代码实现,因为底层数据结构是双向链表,无需像ArrayList那样指定初始长度。
在这里插入图片描述
第二个构造方法,可以传入Collection接口实现类的集合对象,将集合内的元素使用addAll添加到LinkedList内部。
在这里插入图片描述

LinkedList常用方法源码

add(E e)(在链表末尾添加元素)

在add()方法里调用linkLast(e)方法在尾部添加元素,然后直接返回true了,我们来看下linkLast()方法。
在这里插入图片描述

linkLast(E e)(在链表尾部添加元素)

/**
 * Links e as last element.
 */
void linkLast(E e) {
    //定义l变量指向linkedList的last尾部节点地址
    final Node<E> l = last;
	//新建一个node节点对象,将新节点的prev指针指向last节点地址引用,当前元素值为e,next节点为空
    final Node<E> newNode = new Node<>(l, e, null);
    //将last变量重新指向新节点的地址引用,last永远指向尾部节点
    last = newNode;
    //判断如果以前的尾部节点为空,就意味着当前链表没有节点
    if (l == null)
        //此时就直接将first指向新节点的地址引用,新节点就作为头节点
        //这种就是只有一个节点,头尾都指向这个节点
        first = newNode;
    else
        //之前的尾部节点不为空的话,就将之前尾部节点的next指针指向新节点地址引用
        //因为是双向链表,之前已经将新节点的prev指向旧的尾节点,
        //在这里是将旧的尾节点的next指针指向新节点
        l.next = newNode;
	//链表长度加一
    size++;
    //结构修改次数加一 快速失败机制
    modCount++;
}

接下来我们画图来展示下这个过程:

在这里插入图片描述

add(int index, E element)(在index索引位置添加元素)

在这个方法里先检查index是否越界(index >= 0 && index <= size),如果越界直接抛异常,没有越界接着判断index == size,如果index与链表大小size相等就是在链表末尾添加元素,直接调用linkLast()方法即可,上面已经分析过了,否则就调用linkBefore(element, node(index))方法在index位置添加元素。这里又涉及到两个新方法,我们一个一个的看。
在这里插入图片描述

node(int index)(寻找index位置对应的节点)

我们先来看下node(index)寻找节点方法是怎么实现的,看下面的代码部分。

/**
 * Returns the (non-null) Node at the specified element index.
 */
Node<E> node(int index) {
    // assert isElementIndex(index);
	//判断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;
    }
}

这里有一个需要关注的地方,我们知道双向链表的遍历,只能从头节点一直寻找next指针来往后遍历,或者从尾节点通过prev指针往前遍历,也就是说链表只能顺序遍历,不支持随机访问,这样就有一个问题,如果我们要找的元素下标index在很后的位置时,从头节点一直往后遍历寻找,相当于找遍整个链表才结束,时间复杂度为O(n),如果链表很长的话,查询效率肯定不高,jdk设计者们肯定也想到这一点,所以他们对此作了优化,先判断index在链表的前半部分还是后半部分,如果比较靠前就通过first头节点往后遍历,如果比较靠后就通过last尾节点往前遍历,将时间复杂度变为O(n/2),提高了查询效率。

linkBefore(E e, Node succ)(在succ节点前添加新节点元素)

 /**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //定义pred变量指向succ的prev前驱节点
    final Node<E> pred = succ.prev;
    //新建一个节点,让其prev指针指向succ的前驱节点pred,next指针指向succ节点
    //当前元素值为e
    final Node<E> newNode = new Node<>(pred, e, succ);
    //将succ节点的prev指针断开之前指向的前驱节点,重新指向新节点
    succ.prev = newNode;
    //判断succ的旧前驱节点是否为空
    if (pred == null
        //如果是空,意味着之前的first就指向的是succ节点,也就是succ是头节点
        //此时在succ节点前插入一个新节点了,那么first指针就指向这个新节点
        first = newNode;
    else
        //如果不为空,就将旧的前驱节点的next指向新节点
        pred.next = newNode;
    //链表长度加一
    size++;
    //结构修改次数加一 快速失败机制
    modCount++;
}

同样,我们画图展示下这个插入的过程:

在这里插入图片描述

get(int index)(获取index位置上的元素值)

先检查一下index是否越界,然后再调用node(index)方法获取index位置上的节点,该节点的item就是元素值。node(index)方法在上面已经分析过,就不再看了。
在这里插入图片描述

indexOf(Object o)(寻找对象o在链表中第一次出现的下标索引)

public int indexOf(Object o) {
    //定义index变量初始化为0
    int index = 0;
	//判断对象o是否为null
    if (o == null) {
        //如果为null,从链表头节点往后依次遍历,
        for (Node<E> x = first; x != null; x = x.next) {
            //如果发现节点的item值为null就直接返回index
            if (x.item == null)
                return index;
            //每遍历一次,index下标加一
            index++;
        }
    } else {
        //不为空,从链表头节点往后遍历
        for (Node<E> x = first; x != null; x = x.next) {
            //调用equals()方法判断节点的item是否与对象o相等
            //相等直接返回index
            if (o.equals(x.item))
                return index;
            //每遍历一次,index下标加一
            index++;
        }
    }
    //遍历结束都没有找到,就返回-1
    return -1;
}

lastIndexOf(Object o)(寻找对象o在链表中最后一次出现的下标索引)

与indexOf()方法类似,区别在于lastIndexof()方法是从链表尾节点往前遍历寻找,其余逻辑一样。

public int lastIndexOf(Object o) {
    //定义index变量初始化为链表的末尾
    int index = size;
	//判断对象o是否为null
    if (o == null) {
        //如果为null,从链表尾节点往前依次遍历,
        for (Node<E> x = last; x != null; x = x.prev) {
            //每遍历一次,index下标往前移动一位
            index--;
            //如果发现节点的item值为null就直接返回index
            if (x.item == null)
                return index;
        }
    } else {
        //不为空,从链表尾节点往前遍历
        for (Node<E> x = last; x != null; x = x.prev) {
            //每遍历一次,index下标往前移动一位
            index--;
            //调用equals()方法判断节点的item是否与对象o相等
            //相等直接返回index
            if (o.equals(x.item))
                return index;
        }
    }
	//遍历结束都没有找到,就返回-1
    return -1;
}

remove(Object o)(在链表中删除对象o)

public boolean remove(Object o) 
	//判断对象o是否为null
    if (o == null) {
        //如果为空,从链表头节点往后遍历
        for (Node<E> x = first; x != null; x = x.next) {
            //如果找到节点x的item为null
            if (x.item == null) {
                //就调用unlink()方法将节点从链表中移除
                unlink(x);
                //移除成功返回true
                return true;
            }
        }
    } else {
        //不为空,就通过equals()方法比对是否相等
        for (Node<E> x = first; x != null; x = x.next) {
            //如果找到x节点的item与对象o相等
            if (o.equals(x.item)) {
                //就调用unlink()方法将节点从链表中移除
                unlink(x);
                //移除成功返回true
                return true;
            }
        }
    }
	//遍历结束没有找到,返回false
    return false;
}

可以看到LinkedList的remove(o)方法会在内部调用unlink(x)方法移除节点x,我们接下来看下unlink(x)方法的具体实现。

unlink(Node x)(在链表中移除节点x)

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    // assert x != null;
    //定义element变量保存节点x的item元素,便于方法返回
    final E element = x.item;
    //定义一个next变量引用指向节点x的next节点(后继节点)
    final Node<E> next = x.next;
    //定义一个prev变量引用指向节点x的prev节点(前驱节点)
    final Node<E> prev = x.prev;

    //判断节点x的prev节点是否为空
    if (prev == null) {
        //如果为空的话说明节点x就是链表的头节点,first一开始指向节点x
        //现在要删除节点x,那么就将first指针重新指向节点x的next节点,让其变为新的头节点
        first = next;
    } else {
        //如果不为空,就将节点x的prev节点(前驱节点)的next指针指向节点x的next节点(后继节点)
        prev.next = next;
        //再将节点x的prev指针指向null
        //让节点x与它之前的prev节点断开
        x.prev = null;
    }

    //判断节点x的next节点是否为空
    if (next == null) 
        //如果为空说明节点x就是链表的尾节点,last一开始指向节点x
        //现在要删除节点x,那么就将last指针重新指向节点x的prev节点,让其变为新的尾节点
        last = prev;
    } else {
        //如果不为空,将节点x的next节点(后继节点)的prev指针指向节点x的prev节点(前驱节点)
        next.prev = prev;
        //再将节点x的next指针指向null
        //让节点x与它之前的next节点断开
        x.next = null;
    }

	//将节点x的item置为null
    x.item = null;
	//链表数量size减一
    size--;
	//结构修改次数加一
    modCount++;
	//返回旧元素值
    return element;
}

老规矩,我们用画图展示下这个删除节点的流程:

在这里插入图片描述
上面都是实现的list类型接口的一些方法,接下来我们再来看下LinkedList作为Deque双端队列的一些方法实现。

getFirst()(获取队列头节点元素)

方法里直接返回first指针指向的节点的元素值。
在这里插入图片描述

getLast()(获取队列尾部节点元素)

同理,getLast()方法返回last指针指向的节点的元素值。
在这里插入图片描述

peek()/peekFirst()(获取队列头部的节点元素,不会删除节点)

在方法里判断first指针是否指向null,不为null返回first指针指向节点item元素值,否则返回null。
在这里插入图片描述
在这里插入图片描述

peekLast()(获取队列尾部的节点元素,不会删除节点)

在方法里判断last指针是否指向null,不为null返回last指针指向节点item元素值,否则返回null。
在这里插入图片描述

offer(E e)(往队列尾部添加元素)

方法内部调用add(e)方法往队列尾部添加元素,add()方法上面已经讲过了就不再叙述了。
在这里插入图片描述

offerFirst(E e)/push(E e)(往队列头部添加元素)

方法内部调用addFirst(e)方法,addFirst(e)方法内部又调用了linkFirst(e)方法,我们直接来看下linkFirst(e)方法的具体实现。
在这里插入图片描述
在这里插入图片描述

linkFirst(E e)(在链表头部添加元素)

/**
 * Links e as first element.
 */
private void linkFirst(E e) {
    //定义f变量引用指向first指针指向的节点地址
    final Node<E> f = first;
	//新建一个节点,新节点的prev指针指向null,next指针指向first指针指向的节点地址,
	//当前元素值为e
    final Node<E> newNode = new Node<>(null, e, f);
	//将first指针重新指向新节点的地址,让新节点变为链表的头节点
    first = newNode;
	//判断first之前指向的节点,即之前的头节点是否为空
    if (f == null)
        //如果之前的头节点为空,说明整个链表一开始是没有节点的
        //现在新建的节点就是链表唯一的节点
        //上一步已经把first指向newNode了,现在只需要把last也指向新节点的地址就行了
        last = newNode;
    else
        //如果不为空的话,就将之前头节点的prev指针重新指向新节点的地址
        //让新节点在旧节点的前面
        f.prev = newNode;
	//链表长度加一
    size++;
	//结构修改次数加一
    modCount++;
}

我们再画一张图展示下这个添加节点的过程:

在这里插入图片描述

offerLast(E e)(往队列尾部添加节点元素)

方法内部调用addLast(e)方法,addLast(e)方法内部又调用了linkLast(e)方法,linkLast(e)方法上面已经讲过就不再说了。
在这里插入图片描述

pollFirst()/poll()(从队列头部移除一个节点并返回节点元素值,会移除节点)

方法里判断头节点是否为空,如果为空直接返回null,否则调用unlinkFirst(f)方法将头节点从队列(链表)中移除。接下来分析一下unlinkFirst(f)这个方法。
在这里插入图片描述
在这里插入图片描述

unlinkFirst(Node f)(从链表头部移除头节点)

/**
 * Unlinks non-null first node f.
 */
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //定义element变量存储节点f的元素值,便于方法返回
    final E element = f.item;
    //定义next变量指向节点f的next指针指向的节点
    final Node<E> next = f.next;
    //将节点f的元素清空
    f.item = null;
    //将节点f的next指针指向null,
    //帮助gc回收,把节点f移除后,后续gc垃圾收集器会回收节点f占用的内存
    //回收的条件就是从gc root根都没有任何引用指向节点f,并且节点f也没有指向任何别的节点
    //不然的话,收集器会任务节点f不是垃圾对象就不会回收了。
    f.next = null; // help GC
    //将first指针重新指向为next变量指向的节点地址,就是节点f的下一个节点,让其变为新的头节点
    first = next;
    //判断旧的头节点f的下一个节点是否为空
    if (next == null)
        //如果为空,说明当时链表只有一个节点f,first和last都指向的节点f,
        //现在要把节点f删除了,链表就没有节点了,first和last指针都需要指向null
        last = null;
    else
        //如果不为空,就将新的头节点的prev指针指向null
        next.prev = null;
    //链表长度减一
    size--;
    //结构修改次数加一
    modCount++;
    //返回老的元素值
    return element;
}

一样,我们画图理解下这个流程:

在这里插入图片描述

pollLast()(从队列尾部移除节点元素)

方法里判断尾节点是否为空,如果为空直接返回null,否则调用unlinkLast(l)方法将尾节点从队列(链表)中移除。接下来分析一下unlinkLast(l)这个方法。
在这里插入图片描述

unlinkLast(Node l)(从链表尾部移除尾节点)

/**
 * Unlinks non-null last node l.
 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    //定义element变量保存节点l的item元素值,便于方法返回
    final E element = l.item;
    //定义prev变量指向节点了的prev指针指向的节点地址
    final Node<E> prev = l.prev;
    //清空节点了的item数据
    l.item = null;
    //将节点l的prev指针指向null
    l.prev = null; // help 
    //将last指针重新指向到节点l的前一个节点,让其变为新的尾节点
    last = prev;
    //判断节点l的前一个节点是否为空
    if (prev == null)
        //如果为空,说明链表当时只有节点l一个节点存在
        //first和last指针都指向了节点l
        //现在要删除节点l了,链表就为空了,就要将first和last指针都指向null
        first = null;
    else
        //如果不为空就将节点l的前一个节点的next指针指向null
        prev.next = null;
    //链表长度减一
    size--;
    //结构修改次数加一
    modCount++;
    //返回旧元素值
    return element;
}

下图展示了上述代码的逻辑流程:

在这里插入图片描述

pop()/remove()(从队列头部移除一个元素)

方法内部调用removeFirst()方法,removeFirst()内部调用unlinkFirst(f)方法从队列头部移除节点元素,unlinkFirst(f)这个方法已经分析过了。
在这里插入图片描述

LinkedList总结

  1. LinkedList与ArrayList一样都是线程不安全的。
  2. LinkedList底层数据结构是链表,是通过prev和next指针引用寻找前后节点,因此LinkedList的存储空间可以是不连续的,而ArrayList的必须是连续空间的一个数组。
  3. 相比ArrayList而言,LinkedList的查找效率要比ArrayList的效率低,链表是需要从头节点或者尾节点依次遍历得到目标节点,而数组直接可以通过下标索引得到,但是LinkedList的插入往往会ArrayList更快,链表的插入只要修改prev和next等指针的引用地址就行,而数组是需要重新拷贝一份数据的。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xkmolod

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值