基于JDK1.8的LinkedList源码分析

概述

LinkedList本质上是一个基于双向链表实现 的数据结构,因此,LinkedList中保存的各个Item元素也是有序 的,同时也是可以重复 并且可以为null 的。同时因为他是基于双向链表的实现,因此他在存储元素的过程中,不需要像ArrayList那样去动态的扩容。 另外,LinkedList是非线程安全的,因此,在面向多线程并发业务时,需要注意。当然解决方法也是有的,而且很简单,详见 《基于JDK1.8的ArrayList源码分析》一文中"总结"部分。

LinkedList的定义

csharp 复制代码
public class LinkedList<E>extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    ......
}

从源码可以看出,LinkedList实现了List, Cloneable, java.io.Serializable接口,这三个接口的作用在《基于JDK1.8的ArrayList源码分析》一文中已经详细讲解过了,这里不再赘述。另外,他还实现了Deque接口,该接口让LinkedList具备了双向链表的数据结构的能力,并为其提供了基础功能。

但相比于ArrayList,LinkedList因为没有实现RandomAccess接口,因此他不具备数组那样的通过下标直接高效的访问指定位置Item的能力,他只能通过迭代的方式,去访问满足条件的Item元素。

注意:

dart 复制代码
不是LinkedList不具备像ArrayList那样通过下标访问(get(int position))指定位置Item的能力,
而是说通过LinkedList通过下标访问指定位置的Item的效率非常低,因为他必须通过指针一个一个的
移动到指定位置去,速度很慢,效率很低;而通过Iterator迭代器访问的话,则效率很高。

LinkedList的优缺点:

优点:

因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高不需要批量扩容也不需要预留空间 ,所以空间效率ArrayList

缺点:

就是需要随机访问 元素时,时间很慢,效率很低 ,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询以提升时间效率。不过,总体效率依然很低。

LinkedList 双向链表成员变量

LinkedList的每个节点上有三个字段:

kotlin 复制代码
当前节点的数据字段(data);
指向上一个节点的字段(prev);
和指向下一个节点的字段(next);

而其每个节点的成员变量如下:

kotlin 复制代码
private static class Node<E> {
    E item; // 当前节点
    Node<E> next; // 当前节点的下一个节点
    Node<E> prev; // 当前节点的上一个节点

    Node(Node<E> prev, E element, Node<E> next) {
        // 这里的 this 指的是 当前节点对象 Node
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

因此,每个节点上有三个字段:当前节点的数据字段(item),指向上一个节点的字段(prev),和指向下一个节点的字段(next)。

LinkedList的成员变量

vbscript 复制代码
public class LinkedList<E>extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    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;
    ......
}

LinkedList的3个成员变量都被transient关键字修饰,表示LinkedList在被序列化时,他们不参与/不被序列化

之所以 LinkedList 要保存链表的第一个节点和最后一个节点是因为,链表数据结构相对于数组数据结构,优点 在于增删缺点 在于查找

保存了LinkedList 的头尾两端(first/last)节点,当以索引index来查找Item元素的时候,就可以根据 indexsize/2 的大小,来决定从头部节点查找还是从尾部节点查找(类似于二分法查找),这也算是一定程度上弥补了链表数据结构不适合直接通过索引index来查找Item元素的缺点。

LinkedList构造函数

LinkedList无论是无参构造函数还是有参构造函数,都非常简单:

scss 复制代码
/**
 * Constructs an empty list.
 */
public LinkedList() {
}

/**
 * Constructs a list containing the elements of the specified collection, 
 * in the order they are returned by the collection's iterator.
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

从上面源码看,实现非常简单,LinkedList的无参构造函数,实现直接是空的,啥也没做;有参构造函数内部则是先调用LinkedList的无参构造函数,然后再执行addAll(c)操作,将入参Collection集合c添加到链表里面去;而addAll(c)内部又调用了 addAll(size, c) 方法将Collection集合c添加到指定的位置size上。

因此,这里我们可以这样理解:如果想让集合在LinkedList尾部插入,就调用addAll(c)方法;如果是想让集合在指定位置插入,就调用addAll(size, c) 方法。

addAll(c)方法内部直接调用的是addAll(size, c) 方法,下面我们看看addAll(size, c) 方法的源码实现:

从上面的源码实现逻辑,可以知道LinkedList 批量添加节点实现大体分下面几个步骤:

perl 复制代码
1、检查索引值index是否在LinkedList长度范围内,不在的话,就抛出下标越界异常
2、保存 当前节点index 位置 和 上一个节点index-1 位置。
3、将入参Collection集合c转化为数组,通过for循环将数组中的元素封装为节点Node添加到LinkedList链表中。
4、更新链表长度;更新modCount表示LinkedList链表发生了结构性的变化;
5、返回 true 表示添加成功。

6、链表批量增加,是靠for循环遍历原数组,依次执行插入操作。ArrayList是通过System.arraycopy()方法完成批量插入的。
7、通过下标index获取某个node节点的时候,会根据index处于前半段还是后半段,进行一个折半,以提升查询效率(类似文章开头提到的二分法查找)

LinkedList的增删查改

LinkedList添加单个节点

LinkedList添加单个节点有三种种场景:

scss 复制代码
1、执行添加add(E e),不指定添加方式,这时默认是添加在链表的末尾;
2、添加在LinkedList链表的链头addFirst(E e);
3、添加在LinkedList链表的指定的index下标的位置add(int index, E element);
1、执行添加add(E e)
java 复制代码
// 添加在链表的末尾
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 添加单个元素在链表的链尾节点位置
void linkLast(E e) {
    // 暂存之前的末节点
    final Node<E> l = last;
    // 以添加的元素e为节点,构建新的末节点newNode,并将新节点prev指针指向之前的末节点last
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾部节点, 将last索引指向末节点last
    last = newNode;
    // 如果添加之前链表为空,则新节点newNode也作为头节点first
    if (l == null) {
        first = newNode;
    } else { // 否则将之前的未节点first 的next指针指向新节点newNode
        l.next = newNode;
    }
    size++; // 更新LinkedList链表的长度
    modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}
2、添加在链头addFirst(E e)
java 复制代码
// 添加单个元素在链表的头节点位置
public void addFirst(E e) {
    linkFirst(e);
}
// 添加在链表的末尾
private void linkFirst(E e) {
    // 添加Item元素之前的头节点
    final Node<E> f = first;
    // 以添加的元素e为节点,构建新的头节点newNode,并将 下一个节点next 指针指向 之前的头节点
    final Node<E> newNode = new Node<>(null, e, f);
    // first节点指向将新的节点newNode
    first = newNode;
    // 如果添加之前链表空,则新的节点newNode也就是末节点last
    if (f == null) {
        last = newNode;
    } else { // 否则之前头节点的 prev 指针指向新的节点newNode
        f.prev = newNode;
    }
    size++; // 更新LinkedList链表的长度
    modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}
3、添加在指定位置add(int index, E element)
ini 复制代码
// 添加在指定的下标索引index位置
public void add(int index, E element) {
    // 这里检查给定的下标索引index是否是在[0, size]范围内,如果不是,则抛出下标索引index越界异常
    checkPositionIndex(index);
    // 如果指定的下标索引index等于 size,说明是将新的元素添加在链表的末尾。
    if (index == size) {
        linkLast(element);// 将新的元素添加在链表的末尾
    } else {
        linkBefore(element, node(index));
    }
}

// 根据index,获取非空节点Node,这个非空节点位于 index 位置
Node<E> node(int index) {
    //通过下标获取某个node 的时候,会根据index处于前半段还是后半段 进行一个折半,以提升查询效率。类似二分法查找。
    if (index < (size >> 1)) { // 如果 index > size/2 则从size/2到size区间内开始寻找指定角标的节点
        Node<E> x = first;
        for (int i = 0; i < index; i++) {
            x = x.next;
        }
        return x;
    } else { // 否则在 0到size/2的区间内查找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--) {
            x = x.prev;
        }
        return x;
    }
}

// 在指定的 succ 节点 前,插入新的元素节点 e
void linkBefore(E e, Node<E> succ) {
    // 由于 succ 一定不为空,所以可以直接获取 prev 节点
    final Node<E> pred = succ.prev;
    // 新节点 prev 节点为 pred,next 节点为 succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 原节点的 prev 指向新节点
    succ.prev = newNode;
    // 如果 pred 为空即头节点出插入了一个节点,则将新的节点赋值给 first 索引
    if (pred == null) {
        first = newNode;
    } else { // 否则 pred 的下一个节点改为新节点
        pred.next = newNode;
    }
    size++; // 更新LinkedList链表的长度
    modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}

在node(int index)方法中,我们提到根据index,获取非空节点Node,这个非空节点位于 index 位置,这里之所以确定node(int index)方法返回的节点为非null,是因为:

scss 复制代码
1、如果索引index不在[0,size]范围内,那么在checkPositionIndex(index)方法进行index的合法性检测时就会抛出异常;
2、如果一开始链表中没有元素,表示LinkedList链表为空(Empty,非null),此时size = 0,如果这时向索引 `index = 0` 的位置添加元素,那么if判断:
        if (index == size) {
            linkLast(element);
        } else {
            linkBefore(element, node(index));
        }
if条件成立,这时直接调用 `linkLast(element);` 方法去执行添加元素的逻辑。不会走到linkBefore()-->Node<E> node(int index)里面去。

LinkedList.addAll()

LinkedList的批量添加方法addAll(Collection<? extends E> c),内部调用的是addAll(int index, Collection<? extends E> c)方法,这跟有参构造时的执行逻辑是一样的。这里就不再赘述。

LinkedList的修改set()

perl 复制代码
public E set(int index, E element) {
    checkElementIndex(index); // 判断下标索引index是否越界
    Node<E> x = node(index); // 通过下标索引,调用node(int index)方法获取指定位置的节点信息
    E oldVal = x.item; // 暂存指定位置的节点信息
    x.item = element; // 用新的节点信息替代下标索引位置的节点
    return oldVal; // 返回原下标索引位置的节点信息
}

LinkedList的查询

LinkedList链表的查询分以下几种场景:

scss 复制代码
1、查询指定索引index处的节点信息:E get(int index);
2、查询LinkedList链表头的节点信息:E getFirst();
3、查询LinkedList链表尾的节点信息:E getLast();
4、查询某个节点信息在LinkedList链表中的位置:indexOf(Object o);
5、查询LinkedList链表中是否包含某个节点信息:boolean contains(Object o);
1、查询指定索引index节点get(int index)
perl 复制代码
public E get(int index) {
    checkElementIndex(index); // 判断下标索引index是否越界
    // 通过下标索引,调用node(int index)方法获取指定位置的节点信息,然后返回该节点信息的Item元素
    return node(index).item;
}

node()方法前面已经详细分析过了,这里不再赘述,后面也不再提示;

2、查询链表头节点getFirst()
csharp 复制代码
public E getFirst() {
    final Node<E> f = first; // 直接获取LinkedList链表的成员变量first作为链表的头结点
    if (f == null) { // 头结点为null,抛出异常
        throw new NoSuchElementException();
    }
    return f.item;
}
3、查询链表尾节点getLast()
csharp 复制代码
public E getLast() {
    final Node<E> l = last; // 直接获取LinkedList链表的成员变量last作为链表的头结点
    if (l == null)
        throw new NoSuchElementException(); // 尾结点为null,抛出异常
    return l.item;
}
4、查询某个节点的位置indexOf()

查询某个节点信息在LinkedList链表中的位置,有两种查询方式,一是从头结开始往尾节点查询;另一种是从尾结点开始向头结点查询;

从头结点开始往尾节点查询的实现:

从尾结点开始向头结点查询的实现:

这两个方法有一个缺陷:

sql 复制代码
如果LinkedList链表中,有多个null或者Empty节点对象时,该方法只能查找到第一个。查找不到其他的。
5、查询链表是否包含某个节点contains(Object o)
typescript 复制代码
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

该方法实现很简单,直接调用indexOf(o)方法怕 从头结点开始向尾结点,通过 for循环注意查询链表中是否有指定节点的信息。

LinkedList的删除

LinkedList链表的删除,分以下几种场景:

scss 复制代码
1、删除默认操作:remove();
2、删除链表的第一个节点:removeFirst()/pop();
3、删除指定的元素:remove(Object o)/removeFirstOccurrence(Object o);
4、删除指定的下标索引index对应的节点元素:remove(int index);
5、删除链表的最后一个节点:removeLast();
移除第一个remove()

对于场景1和场景2,其实最后执行的都是removeFirst()方法,下面我们看看remove()方法内部的执行情况:

csharp 复制代码
public E remove() {
    return removeFirst();
}

remove(o)方法内部调用的是removeFirst()方法,我们再看看pop()方法内部的执行情况:

csharp 复制代码
public E pop() {
    return removeFirst();
}

pop()方法内部调用的也是removeFirst()方法。因此我们来看看removeFirst()方法的实现:

ini 复制代码
public E removeFirst() {
    final Node<E> f = first; // 获取链表的第一个节点 first,他是链表的成员变量
    if (f == null) {
        throw new NoSuchElementException();
    }
    return unlinkFirst(f); // 调用unlinkFirst()方法执行remove移除操作
}

private E unlinkFirst(Node<E> f) {
    final E element = f.item; // 暂存 f 节点的 元素信息
    final Node<E> next = f.next; // 暂存 f 节点的 下一个节点的信息
    // TODO:下面的 f 清空及后面的if...else...操作完成后,GC将会回收节点内存。
    f.item = null; // 清空 f 节点的的元素信息
    f.next = null; //  // 清空 f 节点的的下一个节点的信息
    first = next; // 重新将 原本的next 下一个节点的信息赋值给first,使其成为新的头结点
    if (next == null) { // 如果 next 节点信息为空,表示后面first节点后面没有其他的节点了,清空 last 节点
        last = null;
    } else { // next 节点信息不为空,那么就将原本的next节点的上一个节点清空(s上一个节点就是被删除的那个节点)
        next.prev = null;
    }
    size--; // 更新链表的长度
    modCount++; // 更新modCount,表示链表发生了结构性改变。
    return element;
}

unlinkFirst()方法主要干了以下几件事情:

1、暂存要移除的节点元素item及要移除的节点的下一个节点next;

2、清空要移除的节点的元素信息item、下个节点的信息next,将要移除的节点的下一个节点next赋值给链表的第一个节点first;

3、清空next节点及对应的上一个节点;

4、更新 size和modCount;

删除指定元素remove(Object o)

删除指定元素有两个实现方法,分别是removeFirstOccurrence(Object o)方法和remove(Object o)方法,但其实removeFirstOccurrence(Object o)方法内部调用的就是remove(Object o)方法:

typescript 复制代码
public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}

因此我们看看remove(Object o)方法的内部实现:

删除链表所有元素clear()
转化为数组toArray()

其内部就是通过for循环将LinkedList的所有节点都保存到一个长度为size的Object数组中:

ini 复制代码
public Object[] toArray() {
    Object[] result = new Object[size];
    int i = 0;
    for (Node<E> x = first; x != null; x = x.next) {
        result[i++] = x.item;
    }
    return result;
}

总结:

1、LinkedList 是双向列表的数据结构实现。因此,LinkedList中保存的各个Item元素是有序 的,可以重复 的,并且可以为null的。同时因为他是基于双向链表的实现,因此他在存储元素的过程中,不需要像ArrayList那样去动态的扩容。

2、链表批量增加,是通过for循环遍历原Collection数组,依次执行插入节点操作,增加一定会修改modCount,表示LinkedList链表发生了结构性改变。而ArrayList则是通过调用Native层的System.arraycopy()方法完成批量插入的。

3、通过下标index获取某个指定位置的node的时候,会采取类似二分法查找的方式,根据index处于前半段还是后半段进行查询,以提升查询效率

4、通过下标index删除指定节点,会先根据index索引在node()方法里面通过for循环遍历LinkedList链表,找到对应的Node节点信息,然后去链表上unlink掉这个Node。

5、通过元素Object o删除,会先去通过for循环遍历LinkedList链表,确认链表中是否包含该Node节点,如果有,去链表上unlink掉这个Node。

6、但凡对LinkedList链表执行增加、插入、删除操作,就一定会更新modCount参数,表示LinkedList链表发生了结构性变化。

7、修改set()操作,也是先通过node()方法,根据下标index索引通过for循环找到Node,然后替换值。

8、查找get()操作,相比于修改set()操作,少了然后替换值的操作,其他操作一样。

9、修改set()操作和查找get()操作,都不会更新modCount。

相关推荐
m0_7482338811 分钟前
【学一点儿前端】本地或jenkins打包报错:getaddrinfo ENOTFOUND registry.nlark.com
前端·servlet·jenkins
摸鲨鱼的脚12 分钟前
Vue导出报表功能【动态表头+动态列】
前端·javascript·vue.js
海上彼尚28 分钟前
npm、yarn、pnpm三者的异同
前端·npm·node.js
余生H29 分钟前
前端的 Python 入门指南(六):调试方式和技巧对比
开发语言·前端·javascript·python
m0_7482370532 分钟前
前端报错npm ERR cb() never called问题
前端·npm·node.js
818源码资源站43 分钟前
Ripro V5日主题 v8.3 开心授权版 wordpress主题虚拟资源下载站首选主题模板
前端
低代码布道师1 小时前
第二篇:脚手架搭建 — React 和 Express 的搭建
前端·react.js·express
m0_748238781 小时前
前端文件预览整合(一)
前端·状态模式
程序员大金1 小时前
基于SpringBoot+Vue的高校电动车租赁系统
前端·javascript·vue.js·spring boot·mysql·intellij-idea·旅游
莫惊春2 小时前
HTML5 第七章
前端·html·html5