美文网首页
LinkedList源码解析

LinkedList源码解析

作者: 柯基去哪了 | 来源:发表于2018-02-28 09:15 被阅读0次

    1 序

    api文档给出的解释还是可以仔细读,从中可以得到综述信息。

    • 双端队列
    • 实现了list接口的所有操作
    • 允许添加null
    • JDK版本为1.8

    与ArrayList一样,linkedList本身也不是线程安全的。api解释到了,如果在多线程环境下使用list并且没有添加必要的同步代码,那么更推荐使用Collections.synchronizedList

    LinkedList对象获取的遍历器满足fail-fast策略,多线程环境下的使用就要注意了。但是请不要依赖于fail-fast策略来在多线程环境下使用这个容器。这并不能保证避免一些少见、晦涩的异常。到时候就很难排查了。

    基于链表的数据结构,add和remove操作linkedList的表现比ArrayList好,其add(E)与remove()操作的时间复杂度是O(1),get(int)与remove()为O(n)。ArrayList基于动态数组的数据结构与更适合随机访问的场景。

    2 代码

    2.1 类的继承结构

    public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
    

    除了期望中的list、cloneable和序列化之外,还有一个deque队列接口和AbstractSequentialList。

    实现了队列接口意味着linkedList实现了队列数据结构的一系列方法,比如pop/peek/push等等。

    另一个AbstractSequentialList是一个之前从没有去注意过的另一个抽象类。一个list接口的最小化实现。其通过迭代器实现了与随机访问不同的get/set/add等一系列方法。这一系列方法可以被linkedList复用到。

    API中的介绍说明LinkedList的一部分特质是和ArrayList类似的(这些特质也是List接口本身决定的)

    • 允许插入null
    • 允许重复元素

    2.2 链表基础操作

    和在以前数据结构的教材上看到的类似,类中包含了一个头结点和一个尾节点

    /**
     * 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的内部类,类结构本身比较简单,一个前驱节点一个后继节点和一个用来容纳节点内容的泛型对象*/
    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;
    }
    }
    

    链表的插入操作和删除操作都是修改对应前驱和后继节点的指向。这个Node对象是LinkedList的一个内部类,包含一个前驱引用和一个后继引用,这样的数据结构可以帮助实现双端队列

    TailInterpolation.jpg

    2.2.1 构造函数

    总计有两个构造函数,一个无参构造和一个接纳已有集合中元素的构造函数。

    /**
     * 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);
    }
    

    无参构造没有做任何多余的操作。另一个构造函数则会执行addAll方法。这个方法是可以直接调用的,向链表的指定位置插入集合列表。

    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);
        // 集合对象转换为Object对象数组
        Object[] a = c.toArray();
        int numNew = a.length;
        // 判断数组长度,验证有效性
        if (numNew == 0)
            return false;
        // 用于保存原有链表指定位置前后的两个Node节点
        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }
        // for循环遍历对象数组,注释中说明了toArray方法生成的数组排列顺序取决于collection对象遍历器的实现逻辑
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }
        // 将新生成的链表段与之前保存的前驱后缀节点链接起来
        if (succ == null) {
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }
        // 更新链表size字段,该字段表明链表长度
        size += numNew;
        // modCount字段累加
        modCount++;
        return true;
    }
    

    2.2.2 增删改查

    boolean add(E e)

    向链表添加元素,是在其表尾新增一个节点(尾插法)。如果加入的元素是链表中的第一个元素,那么首节点和尾节点会同时指向这个节点元素。

    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        // 新的元素,赋值给当前链表的尾节点
        last = newNode;
        // 如果之前的尾节点为空即当前add方法加入的是这个链表中唯一的一个元素,那么头结点的指向也会指向新加入的节点newNode
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        // 更新链表的长度size字段
        size++;
        // 累加modCount统计字段
        modCount++;
    }
    

    示例为向链表尾加入一个新元素的方法linkLast

    注意Node节点是LinkedList里的一个内部类。结构简洁,包含一个prev,一个next一个泛型化的元素容器类成员item

    同时add操作也对modCount计数做了累加操作。

    public boolean addAll(Collection<? extends E> c)

    这个方法的内部调用非常简洁,直接复用前面已经分析过的方法==addAll(int index, Collection<? extends E> c)==。位置参数直接指定为当前链表的表尾。注意这两个方法都是由public修饰,所以都可以直接使用。

    boolean remove(Object o)

    删除指定元素,那么需要先遍历链表检查是否确实包含了这个目标元素。然后修改该位置元素的指向,包括检查对应的prev和next指向。

    // 注意区分删除的是节点内容为null的节点,不是删除null节点。这个方法的删除遍历顺序是从头节点开始遍历,找到并删除第一个匹配的元素。
    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;
    }
    

    链表的删除操作-解除这个节点在链表中的前驱、后继的关联关系。

    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) {
            // 如果前驱节点为null即当前要删除的节点就是链表的头结点,则将被删除节点的后继节点赋给头结点
            first = next;
        } else {
            // 如果前驱节点不为null则将要删除节点的后继指向删除节点的前驱节点的后继。然后释放被删除节点的前驱引用
            prev.next = next;
            x.prev = null;
        }
    
        if (next == null) {
            // 如果后继节点为null即当前要删除的节点就是链表的尾结点,则将被删除节点的后继节点赋给尾结点
            last = prev;
        } else {
            // 如果后继节点不为null则将要删除节点的前驱指向删除节点的前驱节点的前驱。然后释放被删除节点的后继引用。
            next.prev = prev;
            x.next = null;
        }
        // 将被删除元素的内容item释放
        x.item = null;
        // 更新链表长度的size字段
        size--;
        // modCount统计字段累加
        modCount++;
        // 返回这个被删除的节点的内含元素内容
        return element;
    }
    

    要想在链表中访问到指定位置的元素,需要从头节点开始遍历直到指定下标的位置。这里就和随机访问存在区别和性能差异了。如果链表size很大,那么访问一个元素的操作就会耗时较多。

    链表的删除操作都是将指定位置的元素的头结点指向这个元素的next节点。注意为了方便GC回收释放空间,修改了引用指向后,被删除元素的prev与next指向都一并置空,包括置空元素的item指向。

    E remove(int index)

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

    除了删除指定元素,还有一个删除指定位置的元素方法。这个方法需要首先检查是否有数组越界的潜在危险。找到指定位置上的元素然后执行unlink方法来解除已经存在的引用关系。

    /**
     * Returns the (non-null) Node at the specified element index.
     */
    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;
        }
    }
    

    这个方法在增删查的操作中会被经常用到。获取指定下标位置的非空节点对象。

    E get(int index)

    /**
     * Returns the element at the specified position in this list.
     *
     * @param index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    

    和上一个方法一样,获取指定位置的元素内容,首先检查数组下标是否是在正确的范围内,然后用node(index)遍历获取到目标位置的元素item内容。注意区分linkedList与ArrayList的获取指定下标元素方法的实现,这里可以区别随机访问与顺序访问。

    E set(int index, E element)

    public E set(int index, E element) {
        // 检查下标参数合法性
        checkElementIndex(index);
        // 获取index下标的Node节点
        Node<E> x = node(index);
        // 获取节点原有元素
        E oldVal = x.item;
        // 将新的元素值赋给这个节点
        x.item = element;
        // 返回被替换的节点的值 oldValue
        return oldVal;
    }
    

    设置指定位置的元素。

    跟指定元素位置有关的操作都需要先检查入参下标是否是可用的。下标检查不通过的都会抛出数组越界异常IndexOutOfBoundsException。然后node(index)方法获取指定下标位置的元素,修改该元素的item引用并返回被替换掉的原有的item值。set方法操作成功会返回原有的旧的值。

    void add(int index, E element)

    public void add(int index, E element) {
        checkPositionIndex(index);
    
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    

    向指定位置插入元素

    • 检查入参下标
    • 如果下标是当前list的尾巴上,直接addLast,与add方法的实现一致(尾插法)
    • 如果是在list中嵌入一个元素。那个要对应修改前后节点元素的prev与next的指向。

    顺序表(如ArrayList)插入一个元素后,就要将其后的元素一一进行后移。但是链表只需要修改共计三个节点的前后指向关系。所以这里的操作成本比较,链表是更好的。所以这里就可以看到链表的插入和删除操作的优越性。

    2.3 队列系列操作

    因为Node的实现里面包含了前节点的引用后后续节点的引用,再加上实现了Deque接口,因此LinkedList是一个双端队列的实现。其中的一系列操作方法(在队列首、尾的插入与删除操作)都是基于前面我们提到的增删查改的操作完成的。

    peek()

    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
    

    返回队首元素,不过这个方法不会删除原有队首元素。

    element()

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

    返回队首元素,与前一个方法不同的地方在于,如果当前list列表是一个空列表的话,会抛出异常

    与上面这一组方法类似的还有一组方法,这里放在一起比较。

    E poll()

    E remove()

    这两个方法都是删除队首元素,poll在遇到队列为空的情况下返回null而remove会抛出异常。

    boolean offer(E e)

    向队尾加入元素

    2.4 双端队列系列操作

    包含一系列的双端队列队首队尾插入、获取双端队列队首队尾元素、队首队尾删除等等。这一些列方法都是LinkedList类实现的Deque接口所要求的方法。

    2.5 值得注意的地方

    LinkedList实现了cloneable接口,但是其实现的克隆操作是一个浅拷贝注意ArrayList也仅仅是实现了一个浅拷贝方法

    boolean removeFirstOccurrence(Object o)

    删除list列表中遇到的第一个符合要求的object元素的对象。其实这个方法就是很直接的包装了remove(Object)方法。

    boolean removeLastOccurrence(Object o)

    删除list列表中遇到的最后一个符合要求的object元素的对象。这个方法是前一个方法的另一端。反过来从队列的尾端开始寻找。

    3 总结

    LinkedList的使用,主要可以与ArrayList作对比,二者有各自的适用场景和特点。用一句很简洁的话说:ArrayList适合快速随机访问操作而其插入删除的操作效率不如LinkedList。当然这几句话几乎是背下来的,至于为什么是这个结论,需要结合类的数据结构和思想来总结。

    可以简要对比一下几种主要操作二者的时间复杂度:

    • add(E e) ArrayList是O(n),LinkedList是O(1)
    • remove(int n) ArrayList是O(n),LinkedList是O(1)
    • get(int n) ArrayList是O(1),LinkedList是O(n)

    相关文章

      网友评论

          本文标题:LinkedList源码解析

          本文链接:https://www.haomeiwen.com/subject/ondvxftx.html