LinkedList源码学习

315 阅读7分钟

概述

LinkedList 是列表的另一重要实现,是List和Deque接口的双向链表实现,并允许null元素。由于源码数据结构采用链表的方式实现(持有后继元素的引用),所以该类型列表支持顺序访问。

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

从类的继承结构来看,该类还实现了Deque的接口,支持使用队列或栈的一些操作。

成员变量

// 记录当前列表元素的个数
transient int size = 0;
/**
 * 指向第一个节点的指针(引用)
 * 不变式: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;
/**
 * 指向最后一个节点的指针(引用)
 * 不变式: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

LinkedList 的成员变量相较于 ArrayList 较少,没有用来存储元素的数组,没有空间大小的限制。最重要的是持有两个元素节点的引用,这样才能准确的操作这个双向列表。文档中规定的不变式表示该节点引用必须且永远满足这个表达式。即当头节点为null时,尾节点也必须为null,当前列表就为空;或者当头节点不为null时,头节点的前驱节点是必须为null,且头节点的元素不为null,尾节点也满足类似的规定。

构造函数

LinkedList 提供了一个空参构造器和一个包含集合参数的构造器:

public LinkedList() {
}

/**
 * 按照集合的迭代器返回的顺序构造一个包含指定集合元素的列表
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

空参构造器初始化一个空的列表,第二个构造器初始化一个空列表后,将集合中的元素按照迭代器返回的顺序依次添加到列表中,具体的addAll()方法的代码将在下文介绍。

节点类(Node)

为了实现链表的结构和方便链表的操作,LinkedList 内部维护了一个类Node表示节点,该类只在内部使用,在使用该列表的时候感受不到Node的结构。该类和我们所熟知的双向链表数据结构一样,保存链表元素和前驱后继节点的引用。

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

由于LinkedList中主要方法都是从ListDeque继承而来,接下来就分成两部分分别介绍相关的方法实现。比较特殊的是,两块方法中有些共同的逻辑实现都被抽到了相同的方法内,表示列表和队列的方法都调用相同的方法实现。

列表操作

add(E e)

// 将指定的元素附加到此列表的末尾
public boolean add(E e) {
    linkLast(e);
    return true;
}

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

添加元素的核心逻辑在于linkLast(),此方法表示将一个元素链接到链表尾部,所有包含这个逻辑的方法实现都将调用该方法(addLast)。首先将元素构造成新的尾节点,然后赋值给last变量,判断旧的尾节点如果为null,就表示链表为空,直接将头节点指向新的节点;否则将旧节点的后继节点指向新的节点。

get(int index)

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
// 返回指定元素索引处的(非空)节点
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;
    }
}

我们知道LinkedList的特性不允许随机访问元素,只能从头(尾)节点遍历到指定位置取元素,因此代码中首先是检查了元素的有效性(不再展示源码),接着就是取节点的核心方法node(),该方法也在多处共同调用。遍历的时候做了优化,让索引和size的一半的大小比较,即是索引在链表前半部分的话,从链表头部开始遍历,否则从链表尾部开始遍历,这样提高了整体取值的效率。

remove(int index)

操作索引前仍然要校验元素的有效性,依照链表的特性,删除元素需要先遍历找到元素节点,依旧调用了上文中的node()方法获取节点,然后再将该元素节点与链表断开unlink(Node<E> x)

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) {
        first = next;
    } else {
        prev.next = next;
        x.prev = null;
    }
    if (next == null) {
        last = prev;
    } else {
        next.prev = prev;
        x.next = null;
    }
    x.item = null;
    size--;
    modCount++;
    return element;
}

该方法将一个节点与链表断开,同时将该节点前驱、后继节点互相拼起来组成链表,修改sizemodCount

indexOf(Object o) & lastIndexOf(Object o)

public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

链表查找元素第一次出现的索引也只能通过遍历的方式,从头节点向后遍历,逐个比较元素;lastIndexOf则是从尾节点向前遍历查找元素最后一次出现的索引。

队列操作

LinkedList实现了双端队列接口Deque,因此也具有队列和栈的作用,其中有一些方法的核心实现和列表的一些方法实现共用相同的代码,所以对这些方法简单以表格形式介绍。

队列方法作用介绍
boolean offer(E e)添加元素到列表尾部只调用了add()
E poll()返回并删除队列头部元素调用unlinkFirst(),队列为空就返回null
E peek()仅返回队列头部元素队列为空就返回null
E remove()返回并删除队列头部元素核心调用unlinkFirst(),队列为空就抛异常
E element()仅返回队列头部元素队列为空就抛异常
void push(E e)将元素插入到列表头部,表示压栈调用了linkFirst()
E pop()返回并删除列表头部元素,表示弹栈调用了unlinkFirst()

上述包含了基本的操作队列或者栈的方法,除此之外还有一些类似offerFirstofferLastpeekFirstpollLast等的扩展方法,这些方法的实现核心都来源自linkFirstlinkLastunlinkFirstunlinkLast等方法。linkLast在上文中已经出现过并且和linkFirst逻辑类似,因此这里只对unlink的两个方法介绍。

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

该方法将给定的头节点(调用前确定为first)从链表中删除,只需要将后继节点赋值给first并断开和头节点之间的关联即可,当不存在后继节点删除以后,将last = null表示链表为空,最后更新sizemodCount

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

该方法将给定的尾节点(调用前确定为last)从链表中删除,只需要将前驱节点赋值给last并断开和尾节点之间的关联即可,当不存在前驱节点删除以后,将first = null表示链表为空,最后更新sizemodCount

迭代器操作

迭代器方面,LinkedList只重写了列表迭代器ListItr。内部也改为维护迭代的下一个节点和上一次返回的节点,遍历操作将返回节点元素,并且保存后驱节点到next;删除操作将lastReturned节点与链表断开链接,并将lastReturned = nullexpectedModCount++。同时操作的时候仍然会先对列表进行并发修改判断以保证快速失败机制。

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }
}
public boolean hasNext() {
    return nextIndex < size;
}

public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();
    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
public void remove() {
    checkForComodification();
    if (lastReturned == null)
        throw new IllegalStateException();
    Node<E> lastNext = lastReturned.next;
    unlink(lastReturned);
    if (next == lastReturned)
        next = lastNext;
    else
        nextIndex--;
    lastReturned = null;
    expectedModCount++;
}

LinkedList 的重要内容简单介绍这么多,相较于ArrayList,该类在日常开发使用中并不是那么广泛,但是它仍然提供了很多的操作API适应众多场景(列表、队列、栈),同时在源码设计上也非常巧妙。因此我们在适应的场景中还是要考虑使用LinkedList来创建列表。随着对集合源码学习的深入,我们意识到一些简单类的源码也并非深不可测,只要了解一下文档规范,再真实的去看每一行实现代码,每个方法都能被理解,最后再纵观全局去思考并学习某些巧妙的代码。

原文地址 --- Java容器源码学习分析专题——LinkedList源码学习