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

[toc]
在對ArrayList源碼有過了解之后,現在對LinkedList源碼進行相應的分析。

1.結構及成員變量

1.1基本結構

linkedList本質是實現了一個雙向鏈表。其類繼承關系如下圖:


image.png

可以看到LinkedList繼承了AbstractSequentialList,實現了List<E>, Deque<E>, 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,其構成如下圖:


image.png

我們可以看到其內部的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節點。
此時通過l==null判斷此時list中是否為空,如果l==null則說明沒有任何元素,那么first的指針也會指向newNode。反之則將l的next指向newNode。
需要注意的是,這種修改會到modCount增加。這是實現fail-fast機制的基礎。
modCount 在其繼承的抽象類中。
此過程可以通過如下圖表示:
假定我們在linkedList中插入 1 和 2。我們來看看這個過程。
首先我們在LinkedList中添加1,由于最開始這個List為空,因此添加1的時候會導致fist和last都指向這個節點,Node上的next和prev值為空。

image.png

當我們再次添加節點2的時候,再來看這個過程。
在這個時候,l的值等于last,指向了節點1。然后new一個新的節點2,這一步的時候,node2的prev指針就指向了l。再將last指向這個新的node。此時如下所示:

image.png

再此之后再判斷,l此時不為null,那么將l的next指針指向這個新的node。


image.png

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


image.png

上面即使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指向這個元素。


image.png

定義一個指針pred指向succ的prev節點。之后創建一個新節點,其prev為pred指向的節點,next為succ指向的節點。


image.png

此時將succ的prev指針指向newNode。


image.png

再進行判斷,此時pred不為null,所以將pred的next指針指向newNode。


image.png

這樣添加操作就執行完成,對size和modCount進行加1操作。之后再回收變量。操作完成之后的鏈表如下:


image.png

這樣一個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的性能進行分析。也許會改變一直以來大家的認知。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,983評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,772評論 3 422
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,947評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,201評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,960評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,350評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,406評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,549評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,104評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,914評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,089評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,647評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,340評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,753評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,007評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,834評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,106評論 2 375