LinkedList源码解析

2018-02-28  本文已影响0人  柯基去哪了

1 序

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

与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接口本身决定的)

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

向指定位置插入元素

顺序表(如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。当然这几句话几乎是背下来的,至于为什么是这个结论,需要结合类的数据结构和思想来总结。

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

上一篇下一篇

猜你喜欢

热点阅读