LinkedList 源码解析

1,847 阅读8分钟

前言

  LinkedList 是 Java 又一种常用的集合类,与 ArrayList 有很大的不同,用链表结构存储元素。本文将对 Java 中的 LinkedList 源码进行全面的梳理,对其节点结构,属性,初始化以及增删改查等过程源码进行详细解析。

链表节点结构

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

  从节点结构可以看出,LinkedList 属于双向链表,存储前驱和后继节点的引用。

属性和初始化

// 元素个数
transient int size = 0;
// 链表头节点
transient Node<E> first;
// 链表尾节点
transient Node<E> last;

public LinkedList() {
}

public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

  属性这块也很简单,size 表示 LinkedList 的元素个数,first 和 last 维护着 LinkedList 的头节点和尾节点。

  初始化也只有两个构造函数,一个是无参的,啥也没做,另一个是初始化时添加集合中的元素到 LinkedList 里,调用addAll(c),这个函数在增加元素里再讲。

增加元素

基本的增加方法

  LinkedList 增加元素的方法特别多,但基本的还是增加头节点,增加尾节点和增加中间节点三个。

增加头节点

private void linkFirst(E e) {
    final Node<E> f = first;
    // 创建新节点,后继为原头节点
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    // 若之前链表为空,设置 last
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

  先创建了一个新节点,item 为指定元素,后继为原头节点;再将此新节点设为 first,之后就是设置原头节点前驱为新节点了。此处需进行判断,如果之前 LinkedList 为空的话,自然无法设置原头节点(null)的前驱,并且 last 也为 null,所以需要设置 last 为新节点,否则,正常设置前驱即可。添加好了之后,更新 size 和 modCount 的值。(这个 modCount 是 AbstractList 的元素,LinkedList 是它的子类,也继承了这个属性,用来表示 LinkedList 被修改的次数)。

增加尾节点

void linkLast(E e) {
    final Node<E> l = last;
    // 创建新节点,前驱为原尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    // 若之前链表为空,设置 first
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

  思路很简单,创建新节点,设置前驱为原尾节点,设置原尾节点后继时,先进行判断,和增加头节点类似的逻辑,不再赘述。

增加中间节点

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    // 创建新节点,后继为 succ,前驱为原succ.prev
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

  源码里有个注释:assert succ != null;,因为调用这个函数之前会做一些判断,保证这个 succ 不为 null。之后创建新节点,后继为 succ,前驱为 succ.prev,接着要设置 succ 的原 prev 的后继了,同样的思想,先判断其是否为空,之后进行相应操作即可。

  这个函数的效果就是在 succ 前面增加了一个新节点。

扩展的增加方法

  由于 LinkedList 实现了 List 和 Deque 两个接口,需要重写它们对应的增加方法,所以有很多版本的增加函数,但都是调用上面介绍的基础增加方法。

public boolean add(E e) {
    linkLast(e);
    return true;
}
public void addFirst(E e) {
    linkFirst(e);
}
public void addLast(E e) {
    linkLast(e);
}
public boolean offer(E e) {
    return add(e);
}
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}
public boolean offerLast(E e) {
    addLast(e);
    return true;
}
public void push(E e) {
    addFirst(e);
}

  非常简单,不解释,也可以看出默认的 add 方法是加到链表尾的。

批量增加

  批量增加最终会调用下面的函数,从指定的 index 开始,批量增加集合里的元素。

public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;
    // 记录 index 处节点及其前驱
    Node<E> pred, succ;
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
    // 在该节点前驱 pred 后链式增加集合里的元素
    // 相当于原链表在 pred 处断开了
    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;
    }
    // 把两条链表合在一起
    // succ 为空,设置 last 即可
    if (succ == null) {
        last = pred;
    } else {
    // succ 不为空,进行两条链表的链接
        pred.next = succ;
        succ.prev = pred;
    }

    size += numNew;
    modCount++;
    return true;
}

  代码看着很长,其实很简单,首先是一些长度检查;通过了之后,找到 index 处的节点,和其前驱 pred,在 pred 后面增加集合中的元素,代码和 linkLast 类似,只不过每次会设置pred = newNode;,保证一直在链表尾增加元素。

  这里其实就相当于原链表从 pred 处断开了,pred 所在的这条链表一直增加元素,增加完了之后,在和 succ 所在的那条链表链接起来,链表的链接非常简单,设置下前驱和后继指针即可。里面有一些 null 的判断,和基本的增加方法比较类似。

  函数的最终效果就是在 index 处的元素前增加给定集合里的所有元素。

删除元素

基本的删除方法

  删除的方法也很多,但基本的还是删除头节点,尾节点和中间节点。

删除头节点

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    // 相应数据设为 null,从而之后GC清理
    f.item = null;
    f.next = null; // help GC
    first = next;
    // 就一个节点,删完了
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

  这里在调用的时候就已经保证传入的 f 为非空头节点。先是把相应的引用设为 null,接着设置新头节点为 next,若 next 为空,说明链表已经空了,设置 last,否则,将 next 前驱设置为空,更新 size 和 modCount 即可。

删除尾节点

private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    final Node<E> prev = l.prev;
    // 相应数据设为 null,从而之后GC清理
    l.item = null;
    l.prev = null; // help GC
    last = prev;
    // 就一个节点,删完了
    if (prev == null)
        first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
}

  同样的,调用的时候保证 l 为非空尾节点,相应引用设为 null,设置新尾节点,进行必要的判空检查,最后更新 size 和 modCount。

删除中间节点

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

  如果节点是中间节点,其实起作用的就是prev.next = next; next.prev = prev;,用这种方式,这个节点在这条链表里就消失了;但是还要考虑特殊情况,比如 prev 或 next 为 null,肯定是无法调用 prev.next抑或是next.prev的。特殊情况特殊判断一下即可。最后更新 size 和 modCount。

扩展的删除方法

  和增加一样,删除也有很多扩展版本,不过基础还是上面三个方法。

public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
public E pollFirst() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}
public E pollLast() {
    final Node<E> l = last;
    return (l == null) ? null : unlinkLast(l);
}
public E pop() {
    return removeFirst();
}

  简单的代码,不解释。

查询元素

按下标查元素

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 的作者把头节点的元素索引看为 0,后面的元素依次类推。查的时候,会判断 index 对应元素在前半部分还是后半部分,从而决定从头节点还是尾节点遍历,这是个小小的优化。

从元素找下标

public int indexOf(Object o) {
    int index = 0;
    // 为 null,直接比引用
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    // 否则,调用 equals
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

  从头开始遍历链表查找相等的元素下标。还有个lastIndexOf,从尾节点开始,不再赘述。

按下标进行增删改

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
public E remove(int index) {
    checkElementIndex(index);
    // 找到之后,删了它
    return unlink(node(index));
}
public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    // 找到之后,修改节点的数据
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}

  均是先调用node(index)找到对应节点,再对该节点进行操作。

迭代器

迭代器的结构

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
    // 当前停留的节点 
    private Node<E> lastReturned;
    // 将要迭代的节点
    private Node<E> next;
    // 将要迭代的 index
    private int nextIndex;
    // 避免并发修改
    private int expectedModCount = modCount;
    
    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }
    ...
}

  和 ArrayList 一样,迭代器本身不保存数据,只是提供一种遍历的机制。用 next 和 nextIndex 来指示下一个要迭代的元素,lastReturned 表示当前停留的节点,expectedModCount 防止并发修改。

next 函数

  迭代器最重要的函数,如下:

public E next() {
    checkForComodification();
    if (!hasNext())
        throw new NoSuchElementException();

    lastReturned = next;
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
public boolean hasNext() {
    return nextIndex < size;
}

  先看 hasNext 方法,return nextIndex < size;,很简单,下一个迭代的元素超出了范围,会返回false。

  调用 next 方法,首先会判断 hasNext。合法的话,设置 next,nextIndex 和 lastReturned 的值,并正确返回。

调用原 LinkedList 的增删函数

  迭代器本身不存储数据,但是可以通过调用原 LinkedList 的增删函数对 LinkedList 进行增删。以 add 为例:

public void add(E e) {
    checkForComodification();
    lastReturned = null;
    if (next == null)
        linkLast(e);
    else
        linkBefore(e, next);
    nextIndex++;
    expectedModCount++;
}

  在最后,有一句 expectedModCount++;,这是因为调用原 LinkedList 的增删函数,会导致modCount++,所以这里要更新对应的 expectedModCount。否则,调用迭代器的函数时,过不了下面的判断:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

  调用迭代器函数,都会进行此判断。所以,如果用迭代器的时候,其它迭代器或原 LinkedList 进行了结构性修改,那么这个迭代器就废了,任何函数都不会调用成功。

forEachRemaining

public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    // 并发修改检查以及不越界
    while (modCount == expectedModCount && nextIndex < size) {
        action.accept(next.item);
        lastReturned = next;
        next = next.next;
        nextIndex++;
    }
    checkForComodification();
}

  对 next 以及之后的节点的 item 属性调用action.accept(next.item);进行操作,Consumer<? super E> action是 Java 提供的函数式接口,如果某次 while 循环里的条件因为modCount != expectedModCount而停止,跳出循环后会调用checkForComodification();,必然会抛出并发修改异常。

  迭代器剩下的函数不再一一介绍了。

克隆方法

  LinkedList 的克隆方法还有点意思,可以讲讲。

private LinkedList<E> superClone() {
    try {
        return (LinkedList<E>) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new InternalError(e);
    }
}
public Object clone() {
    LinkedList<E> clone = superClone();
    // 重置元素
    clone.first = clone.last = null;
    clone.size = 0;
    clone.modCount = 0;
    // 重新初始化
    for (Node<E> x = first; x != null; x = x.next)
        clone.add(x.item);

    return clone;
}

  可能有人会问,调用了LinkedList<E> clone = superClone();不就可以了,为啥要后面那一堆操作。关于 Java 的对象克隆,可以参考我之前的一篇博客:由Arrays.sort()简谈Comparable和Comparator

  总之,如果你简单调用LinkedList<E> clone = superClone();,得到的是非常非常浅的克隆,first 和 last 元素都指向原 LinkedList 的first 和 last 的引用。LinkedList 就是靠 first 和 last 遍历整个链表,进行一系列操作,如果是这样的克隆,那克隆根本没有任何作用,新得到的 LinkedList 只是个原 LinkedList 的别名罢了。

  故 clone 方法后有这个循环:

for (Node<E> x = first; x != null; x = x.next)
    clone.add(x.item);

  别忘了,add 方法会调用 linkLast 方法,该方法有一句final Node<E> newNode = new Node<>(l, e, null);,会创建新节点,这样,克隆的 LinkedList 每个节点都是自己的了,才真正有了一点克隆的作用。当然,两个链表的节点的 item 仍然是一个地方对象的引用,其实还是有点问题的。Java 的作者对此 clone 方法的注释如下:

Returns a shallow copy of this {@code LinkedList}. (The elements
* themselves are not cloned.)

应用

  LinkedList 实现了 Deque 和 List 的接口,可以作为 List,可以作为双端队列,普通队列,可以作为单链表,双链表,由于实现了 pop 和 push 方法,甚至还可以用作栈,可以说用途非常广泛了。

联想到 Redis 的 List 结构

  反正我看完这个 LinkedList 的双向链表实现之后,立刻联想到了 Redis 的 List,Redis 的 List 在节点比较多的时候,也是双向链表实现。

总结

  本文对 LinkedList 的源码进行了全面的梳理,LinkedList 的优势是灵活,可以在两头增删元素,获取链表长度也是 O(1) 的复杂度。比单链表好的地方就是由于每个节点都有前驱,删除中间节点简单,在某节点前增加元素也简单(不需要额外找前驱节点)。这块代码不难,感兴趣可以自己看看,学一学双链表的操作。

PS

  作者已经更新了 ArrayList 和 LinkedList 的源码解析,之后会陆续发 HashMap,HashSet,LinkedHashMap,CopyOnWriteArrayList 等集合的源码解析,敬请关注哦~~