你不知道的LinkedList(一):基于jdk1.8的LinkdeList源码分析

时间:2022-07-23
本文章向大家介绍你不知道的LinkedList(一):基于jdk1.8的LinkdeList源码分析,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

文章目录

在对ArrayList源码有过了解之后,现在对LinkedList源码进行相应的分析。

1.结构及成员变量

1.1基本结构

linkedList本质是实现了一个双向链表。其类继承关系如下图:

可以看到LinkedList继承了AbstractSequentialList,实现了List, Deque, Cloneable, java.io.Serializable。比较特别的是实现了Deque双端队列,这是队列基于链表的一种实现。

/**
 * Doubly-linked list implementation of the {@code List} and {@code Deque}
 * interfaces.  Implements all optional list operations, and permits all
 * elements (including {@code null}).
 *
 * <p>All of the operations perform as could be expected for a doubly-linked
 * list.  Operations that index into the list will traverse the list from
 * the beginning or the end, whichever is closer to the specified index.
 *
 * <p><strong>Note that this implementation is not synchronized.</strong>
 * If multiple threads access a linked list concurrently, and at least
 * one of the threads modifies the list structurally, it <i>must</i> be
 * synchronized externally.  (A structural modification is any operation
 * that adds or deletes one or more elements; merely setting the value of
 * an element is not a structural modification.)  This is typically
 * accomplished by synchronizing on some object that naturally
 * encapsulates the list.
 *
 * If no such object exists, the list should be "wrapped" using the
 * {@link Collections#synchronizedList Collections.synchronizedList}
 * method.  This is best done at creation time, to prevent accidental
 * unsynchronized access to the list:<pre>
 *   List list = Collections.synchronizedList(new LinkedList(...));</pre>
 *
 * <p>The iterators returned by this class's {@code iterator} and
 * {@code listIterator} methods are <i>fail-fast</i>: if the list is
 * structurally modified at any time after the iterator is created, in
 * any way except through the Iterator's own {@code remove} or
 * {@code add} methods, the iterator will throw a {@link
 * ConcurrentModificationException}.  Thus, in the face of concurrent
 * modification, the iterator fails quickly and cleanly, rather than
 * risking arbitrary, non-deterministic behavior at an undetermined
 * time in the future.
 *
 * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed
 * as it is, generally speaking, impossible to make any hard guarantees in the
 * presence of unsynchronized concurrent modification.  Fail-fast iterators
 * throw {@code ConcurrentModificationException} on a best-effort basis.
 * Therefore, it would be wrong to write a program that depended on this
 * exception for its correctness:   <i>the fail-fast behavior of iterators
 * should be used only to detect bugs.</i>
 *
 * <p>This class is a member of the
 * <a href="{@docRoot}/../technotes/guides/collections/index.html">
 * Java Collections Framework</a>.
 *
 * @author  Josh Bloch
 * @see     List
 * @see     ArrayList
 * @since 1.2
 * @param <E> the type of elements held in this collection
 */

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

}

其注释大意为,双向链表实现了List和Deque接口,并实现了所有可选的方法,可以允许元素为null。 对于一个双向链表,所有操作都按照预期那样,索引到列表的操作将从列表开始或者结束位置(以距离索引更近的位置为准)来遍历该列表。 需要注意的是这个链表没有采用synchronized实现。如果多线程并发的访问一个链表,并且至少有一个线程修改了链表的结构,那么它必须采用同步的方式。(结构修改是指添加或者删除一个或者多个元素的任何操作。仅仅设置元素的值并不是结构修改)。这通常是通过对自然封装的列表对象进行同步来实现。 如果没有这样的对象,那么最好是用Collections.synchronizedList方法。

 List list = Collections.synchronizedList(new LinkedList(...));

迭代器是fail-fast方法实现的,如果其结构被修改,那么在使用迭代的过程中将会出现ConcurrentModificationException异常。因此,在并发情况下修改,迭代器是回快速失败。 需要注意的是,迭代器的fail-fast行为不能得到任何保证,因为一般来说,在非同步的并发修改时不可能得到任何担保。

1.2 成员变量

transient int size = 0;

/**
 * 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的成员变量主要有三个,分别是表示链表长度的size,以及链表头和尾的指针first和last。注意这三个变量都是采用transient修饰,序列化的时候这些属性回呗忽略。

1.3 Node

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

这个类支持泛型,然后有两个指针,一个next指向后一个元素,一个指针prev指向前一个元素。

2.LinkedList的数据结构及其其基本操作

2.1基本数据结构

那么我们在对代码进行了解之后,可以发现,LinkedList实际上就是一个以Node节点为基础的双向链表,在这个链表中还有两个指针first和last。假定存在一个元素为1、2、3的linkedList,其构成如下图:

我们可以看到其内部的first和last指针分别指向这个双向链表的首尾位置。然后每个node之间的连接关系也如上图所示。

2.2 构造方法

LinkedList提供两个构造方法,分别是:

    /**
     * Constructs an empty list.
     */
    public LinkedList() {
    }

这是一个空的构造方法,此时Linked的各个指针均为空,只是创建了一个LinkedList的对象。 当然,也可以从一个集合中产生一个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(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    
    public boolean addAll(int index, Collection<? extends E> c) {
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            succ = node(index);
            pred = succ.prev;
        }

        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 += numNew;
        modCount++;
        return true;
    }

需要注意的是此时采用了一个pred指针,用来指向上一次添加的节点,这样再加入新节点的时候就可以直接将pred设置为新增加节点的prev指针。 这个算法可以作为平时刷leetcode的时候的重要参考。

2.3 add(E e)

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #addLast}.
 *
 * @param e element to be appended to this list
 * @return {@code true} (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

实际上这个用得最多的add方法,实际上是调用的尾插法linkLast:

/**
 * 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;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

引入了一个指针l,这个指针先找到last。通过Node的构造方法,将prev指向l,之后变更last为new出来的新的node节点。 此时通过lnull判断此时list中是否为空,如果lnull则说明没有任何元素,那么first的指针也会指向newNode。反之则将l的next指向newNode。 需要注意的是,这种修改会到modCount增加。这是实现fail-fast机制的基础。 modCount 在其继承的抽象类中。 此过程可以通过如下图表示: 假定我们在linkedList中插入 1 和 2。我们来看看这个过程。 首先我们在LinkedList中添加1,由于最开始这个List为空,因此添加1的时候会导致fist和last都指向这个节点,Node上的next和prev值为空。

当我们再次添加节点2的时候,再来看这个过程。 在这个时候,l的值等于last,指向了节点1。然后new一个新的节点2,这一步的时候,node2的prev指针就指向了l。再将last指向这个新的node。此时如下所示:

再此之后再判断,l此时不为null,那么将l的next指针指向这个新的node。

之后再把size加1,modCount加1,方法执行完成并回收局部变量。 最终如下:

上面即使LinkedList再尾部添加一个元素的过程。

2.4 add(int index, E element)

在指定索引的位置插入元素。其代码如下:

/**
 * Inserts the specified element at the specified position in this list.
 * Shifts the element currently at that position (if any) and any
 * subsequent elements to the right (adds one to their indices).
 *
 * @param index index at which the specified element is to be inserted
 * @param element element to be inserted
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

实质上是对index判断是否为size大小,如果与size一致,因为index实际上从0开始,而size从1开始计数,那么就说明应该是直接在尾部插入,直接就调用尾插法即可。反之则调用linkBefore方法。

/**
 * Inserts element e before non-null Node succ.
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = 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++;
}

还需要注意的是此时还有个 node(index)方法。

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

这个方法也是一个在LinkedList中非常常用的方法。实际上是个根据index查找Node的方法。此时如果index小于size的一半,则从链表头开始查找。如果大于size的一般,则从链表尾部开始查找。 而且这个判断的位置采用的是位移计算>>1。这个是我们自己在写代码的时候需要学习的,位移计算的效率是最高的。

在linkBefore方法中,其执行过程如下图所示。 假定我们有一个linkedList数组,其中有1、2、3共3个元素。现在需要将4插入到index为1的位置。 首先,调用node(1)方法,定义一个指针succ指向这个元素。

定义一个指针pred指向succ的prev节点。之后创建一个新节点,其prev为pred指向的节点,next为succ指向的节点。

此时将succ的prev指针指向newNode。

再进行判断,此时pred不为null,所以将pred的next指针指向newNode。

这样添加操作就执行完成,对size和modCount进行加1操作。之后再回收变量。操作完成之后的链表如下:

这样一个linkedList的指定位置插入操作就执行完成。

2.5 get(int index)

我们再看看linked的get方法:

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

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


/**
 * Tells if the argument is the index of an existing element.
 */
private boolean isElementIndex(int index) {
    return index >= 0 && index < size;
}

可以看到在执行过程中首先对index进行判断,是不是一个合法的index。index的范围应该在0-size之间。否则返回IndexOutOfBoundsException异常。 之后再调用上文提到的node方法。

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

判断index与size的一半进行对比。如果小则从前面查找,如果大则从尾部查找。 这个方法的时间复杂度是n。比较低效。

2.6 remove()

remove操作代码如下:

/**
 * Removes the element at the specified position in this list.  Shifts any
 * subsequent elements to the left (subtracts one from their indices).
 * Returns the element that was removed from the list.
 *
 * @param index the index of the element to be removed
 * @return the element previously at the specified position
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

同样需要执行checkElementIndex。之后再执行unlink方法。

/**
 * Unlinks non-null node 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;
}

需要注意的是,unlink方法,中间有个node(index)方法进行查找。这样remove也是一个耗时的过程。 unlink方法本质就是对元素的前后节点指针的修改,之后将移除的元素内部的指针也修改为null以便GC回收,最后size–,modCount再加1。

2.7 其他方法

LinkedList还有很多其他的方法。另外还实现了Deque接口,需要实现一些对队列的操作。

  • addFirst(E e) 在队列头部添加元素。
  • addLast(E e) 在队列尾部添加元素。
  • boolean offerFirst(E e) 在队列头部插入元素,并返回true。
  • boolean offerLast(E e) 在队列尾部插入元素并返回true。
  • removeFirst() 移除队列头部元素。
  • removeLast(); 移除队列尾部元素。
  • E pollFirst() 取出队列头部元素。并在队列中删除。
  • E pollLast() 取出队列尾部元素。并在队列中删除。
  • E getFirst() 得到队列头部元素。不改变队列。如果队列为空 抛出异常。
  • E getLast()得到队列尾部元素,不改变队列。如果队列为空则抛出异常。
  • E peekFirst()得到队列头部元素,不改变队列,如果为空则返回null。
  • E peekLast() 得到队列尾部元素,不改变队列,如果为空则返回null。
  • size() 得到队列的长度。
  • boolean contains(Object o) 判断是否包含某个元素,这个效率比较低下。
  • push(E e) 等价于addFirst。
  • peek() 得到队列头部元素。
  • poll() 得到队列头部元素并移除。

等等,还有很多方法,由于并不常用就不一一介绍了。

3 总结

本文对LinkedList的源码进行了分析,个人认为其链表的操作方法,是我们在leetcode上解决链表问题的时候值得参考的地方。 另外,LinkedList的性能比较低下,我们对LinkedList的理解实际上是存在很多误区的。尤其是查找的过程性能非常低。但是我们对LinkedList的remove等操作又离不开这个寻址过程。个人认为LinkedList除了可以作为队列之外,本身并没有太多的使用价值。 在下一篇文章中,将对LinkedList的性能进行分析。也许会改变一直以来大家的认知。