List接口实现类(2):LinkedList

本文基于jdk8

一、概述

1. 简介

LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。LinkedList实现了所有的列表操作,允许所有的元素(包括空元素)。LinkedList所有的操作都是在对双向链表操作,LinkedList不是线程安全的。Collections.synchronizedList方法可以实现线程安全的操作。

2. 继承体系

在这里插入图片描述
通过继承体系,我们可以看到LinkedList不仅实现了List接口,还实现了Queue和Deque接口,所以它既能作为List使用,也能作为双端队列使用,当然也可以作为栈使用。

3. 数据结构

LinkedList与Collection关系如下图:
在这里插入图片描述

LinkedList的本质是双向链表。
(1) LinkedList继承于AbstractSequentialList,并且实现了Dequeue接口。
(2) LinkedList包含两个重要的成员:header 和 size。
  header是双向链表的表头,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next, element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。
  size是双向链表中节点的个数。

二、源码解析

1. 属性

/**
 * 链表的节点个数
 */
transient int size = 0;

/**
 * 双向链表的首个节点
 */
transient Node<E> first;

/**
 * 双向链表的最后一个节点
*/
transient Node<E> last;

内部类Node

内部类为双向链表的每个节点。item用于保存数据,有一个prev指针和next指针,分别指向链表当前节点的前一个节点和后一个节点

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

2. 构造函数

/**
 * 构造一个空列表。
 */
public LinkedList() {
}

/**
 * C构造一个包含指定 collection 中的元素的列表,
 * 这些元素按其 collection 的迭代器返回的顺序排列。
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList()构造一个空列表,里面没有任何元素。
LinkedList(Collection<? extends E> c): 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中。以下是addAll()的源代码:

/**
 *  添加指定 collection 中的所有元素到此列表的结尾,
 *  顺序是指定 collection 的迭代器返回这些元素的顺序。
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/**
 * 将指定 collection 中的所有元素从指定位置开始插入此列表。
 * 其中index表示在其中插入指定collection中第一个元素的索引
 */
//以index为插入下标,插入集合c中所有元素
public boolean addAll(int index, Collection<? extends E> c) {
    //检查越界 [0,size] 闭区间
    checkPositionIndex(index);
    //拿到目标集合数组
    Object[] a = c.toArray();
    //新增元素的数量
    int numNew = a.length;
    //如果新增元素数量为0,则不增加,并返回false
    if (numNew == 0)
        return false;
    //index节点的前置节点,后置节点
    Node<E> pred, succ; 
    if (index == size) { 
        //在链表尾部追加数据
        //size节点(队尾)的后置节点一定是null
        succ = null;  
        //前置节点是队尾
        pred = last;
    } else {
        //取出index节点,作为后置节点
        succ = node(index);
        //前置节点是,index节点的前一个节点
        pred = succ.prev; 
    }
    //链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。
    //对比ArrayList是通过System.arraycopy完成批量增加的
    for (Object o : a) {//遍历要添加的节点。
        @SuppressWarnings("unchecked") E e = (E) o;
        //以前置节点 和 元素值e,构建new一个新节点,
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null) //如果前置节点是空,说明是头结点
            first = newNode;
        else//否则 前置节点的后置节点设置问新节点
            pred.next = newNode;
        //步进,当前的节点为前置节点了,为下次添加节点做准备    
        pred = newNode;
    }

    //循环结束后,判断,如果后置节点是null。 说明此时是在队尾append的。
    if (succ == null) {
        last = pred; //则设置尾节点
    } else {
        // 否则是在队中插入的节点 ,更新前置节点 后置节点
        pred.next = succ; 
        //更新后置节点的前置节点
        succ.prev = pred; 
    }

    size += numNew;  // 修改数量size
    modCount++;  //修改modCount
    return true;
}

//根据index 查询出Node,
Node<E> node(int index) {
    // assert isElementIndex(index);
    // size >> 1 表示size除以2
    //通过下标获取某个node 的时候,(增、查 ),
    //会根据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;
    }
}

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
    //插入时的检查,下标可以是size [0,size]
    return index >= 0 && index <= size;  
}

执行逻辑:
(1) 使用 this() 调用默认的无参构造函数。
(2) 调用 addAll() 方法,传入当前的节点个数size,此时size为0,并将collection对象传递进去
(3) 检查index有没有数组越界的嫌疑
(4) 将collection转换成数组对象a
(5) 循环遍历a数组,然后将a数组里面的元素创建成拥有前后连接的节点,然后一个个按照顺序连起来。
(6) 修改当前的节点个数size的值
(7) 操作次数modCount自增1
通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率

3. 插入

LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。LinkedList 插入元素的过程实际上就是链表链入节点的过程,比较简单。

/** 在链表尾部插入元素 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

/** 在链表指定位置插入元素 */
public void add(int index, E element) {
    checkPositionIndex(index);

    // 判断 index 是不是链表尾部位置,
    // 如果是,直接将元素节点插入链表尾部即可
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
    final Node<E> l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node<E> newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (l == null)
        first = newNode;
    else
        // 让原尾节点后继引用 next 指向新的尾节点
        l.next = newNode;    
    size++;
    modCount++;
}

/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 1. 初始化节点,并指明前驱和后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 2. 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点    
    if (pred == null)
        first = newNode;
    else
        // 3. succ 节点前驱的后继引用指向新节点
        pred.next = newNode;   
    size++;
    modCount++;
}

上面两个 add 方法只是对操作链表的方法做了一层包装,核心逻辑在 linkBefore 和 linkLast 中。这里以 linkBefore 为例,它的逻辑流程如下:
创建新节点,并指明新节点的前驱和后继
将 succ 的前驱引用指向新节点
如果 succ 的前驱不为空,则将 succ 前驱的后继引用指向新节点

4. 删除

删除操作通过解除待删除节点与前后节点的链接。

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

public E remove(int index) {
    checkElementIndex(index);
    // 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
    return unlink(node(index));
}

/** 将某个节点从链表中移除 */
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;
    
    // prev 为空,表明删除的是头节点
    if (prev == null) {
        first = next;
    } else {
        // 将 x 的前驱的后继指向 x 的后继
        prev.next = next;
        // 将 x 的前驱引用置空,断开与前驱的链接
        x.prev = null;
    }

    // next 为空,表明删除的是尾节点
    if (next == null) {
        last = prev;
    } else {
        // 将 x 的后继的前驱指向 x 的前驱
        next.prev = prev;
        // 将 x 的后继引用置空,断开与后继的链接
        x.next = null;
    }

    // 将 item 置空,方便 GC 回收
    x.item = null;
    size--;
    modCount++;
    return element;
}

和插入操作一样,删除操作方法也是对底层方法的一层保证,删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,考虑到允许null值,所以会遍历两遍,然后再去unlink它。
核心逻辑在底层 unlink 方法中,分析下 unlink 方法的逻辑,如下(假设删除的节点既不是头节点,也不是尾节点):
将待删除节点 x 的前驱的后继指向 x 的后继
将待删除节点 x 的前驱引用置空,断开与前驱的链接
将待删除节点 x 的后继的前驱指向 x 的前驱
将待删除节点 x 的后继引用置空,断开与后继的链接

5. 查询获取get

get方法

首先是判断索引位置有没有越界,确定完成之后开始遍历链表的元素,那么从头开始遍历还是从结尾开始遍历呢,这里其实是要索引的位置与当前链表长度的一半去做对比,如果索引位置小于当前链表长度的一半,否则从结尾开始遍历

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

getfirst方法

直接将第一个元素返回

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

getlast方法

直接将最后一个元素返回

public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

6. 修改set方法

检查设置元素位然后置是否越界,如果没有,则索引到index位置的节点,将index位置的节点内容替换成新的内容element,同时返回旧值。

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

7.push和pop方法

push其实就是调用addFirst(e)方法,pop调用的就是removeFirst()方法。

8. Deque接口

接口Deque的各个方法:
add(E e):队尾插入新节点,如果队列空间不足,抛出异常;LinkedList没有空间限制,所以可以无限添加。
offer(E e):队尾插入新节点,空间不足,返回false,在LinkedList中和add方法同样效果。
remove():移除队头节点,如果队列为空(没有节点,first为null),抛出异常。LinkedList中就是first节点(链表头)
poll():同remove,不同点:队列为空,返回null
element():查询队头节点(不移除),如果队列为空,抛出异常。
peek():同element,不同点:队列为空,返回null。

三、总结

LinkedList是基于双端链表的List,保留了头尾两个指针 ,其内部的实现源于对链表的操作,所以适用于频繁增加、删除的情况;该类不是线程安全的;另外,由于LinkedList实现了Queue接口,所以LinkedList不止有队列的接口,还有栈的接口,可以使用LinkedList作为队列和栈的实现。另外LinkedList也有 failFast 机制,这个机制主要在迭代器中使用。

1. 特点

链表批量增加,是靠for循环遍历原数组,依次执行插入节点操作。对比ArrayList是通过System.arraycopy完成批量增加的。增加一定会修改modCount。
通过下标获取某个node 的时候,(add select),会根据index处于前半段还是后半段 进行一个折半,以提升查询效率
删也一定会修改modCount。 按下标删,也是先根据index找到Node,然后去链表上unlink掉这个Node。 按元素删,会先去遍历链表寻找是否有该Node,如果有,去链表上unlink掉这个Node。
改也是先根据index找到Node,然后替换值。改不修改modCount。
查本身就是根据index找到Node。所以它的CRUD操作里,都涉及到根据index去找到Node的操作。

2. ArrayList与LinkedList的区别

ArrayList本质是数组, LinkedList本质是链表,数组和链表各自的特性 数组和链表的特性差异,本质是:连续空间存储和非连续空间存储的差异。
区别主要有下面两点:
ArrayList:底层是Object数组实现的:由于数组的地址是连续的,数组支持O(1)随机访问;数组在初始化时需要指定容量;数组不支持动态扩容,像ArrayList、Vector和Stack使用的时候看似不用考虑容量问题(因为可以一直往里面存放数据);但是它们的底层实际做了扩容;数组扩容代价比较大,需要开辟一个新数组将数据拷贝进去,数组扩容效率低;适合读数据较多的场合。

LinkedList:底层使用一个Node数据结构,有前后两个指针,双向链表实现的。相对数组,链表插入效率较高,只需要更改前后两个指针即可;另外链表不存在扩容问题,因为链表不要求存储空间连续,每次插入数据都只是改变last指针;另外,链表所需要的内存比数组要多,因为他要维护前后两个指针;它适合删除,插入较多的场景LinkedList还实现了Deque接口。

四、参考

JDK8:LinkedList源码分析:
https://blog.csdn.net/dabusiGin/article/details/102470126

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值