Java集合系列——LinkedList源碼解讀

之前寫過一篇解讀ArrayList源碼的文章(鏈接:Java集合系列——ArrayList源碼解讀),現在接着來寫與之關聯比較近的LinkedList的源碼解讀,對比着學習,體會以不同方式實現的List接口的異同。

概述

簡單說下LinkedList的特點:

  • LinkedList使用雙向鏈表來實現List接口和Deque接口的功能。
  • 實現了所有列表有關的操作,允許添加所有類型的元素(包括null)。
  • 該類是線程不安全的。

接下來開始進行源碼解讀,包括成員變量、構造方法、其他方法。
聲明:本筆記裏的源碼來源於官網安裝文件“jdk-8u201-windows-x64”

成員變量(字段)

transient int size = 0;
說明:指這個鏈表裏面存儲的元素的數量。

transient Node first;
說明:指向鏈表裏第一個結點,即表頭結點。

transient Node last;
說明:指向鏈表裏最後一個結點,即表尾結點。

private static final long serialVersionUID = 876323262645176354L;
說明:LinkedList實現了java.io.Serializable接口,這個字段是針對該接口提供的。

結點結構

凡是談到鏈式結構(不論是列表、樹、圖,只要是鏈式結構實現的),首先都要定義結點,也就是一個結點裏麪包含什麼東西,或者說如何表示一個結點。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裏的一個靜態內部類。
  • 它有三個字段:元素本身(即數據域)、後繼結點、前驅結點。
  • 只有一個構造方法,在構造的時候確定所有字段,順序是:前驅結點、數據域、後繼結點。

構造方法

LinkedList共有兩個構造方法,來看一下。

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

空參數的構造方法:
什麼都沒有做,那說白了就是創建一個LinkedList對象,然後裏面的字段first、last都爲null,size爲0。

有參數的構造方法:
使用一個Collection集合來創建LinkedList,也就是將Collection裏的所有元素裝到LinkedList裏面,這些元素在LinkedList裏的順序,由Collection的迭代器的返回順序來決定。

這裏暫時不分析有參數的構造方法的源碼,因爲這裏涉及到添加一堆元素的操作,我們還是先分析了添加單個元素的操作,再來分析添加一堆元素的操作。

List和Deque的對比

在分析LinkedList的添加元素和移除元素的方法之前,不知道大家有沒有想過,List接口和Deque接口都有添加和移除元素的方法,那麼LinkedList要怎麼實現它們呢?會不會衝突呢?我們接下來對比一下。

List Deque
添加元素 boolean add(E e) 和 void add(int index, E element); boolean add(E e);
移除元素 boolean remove(Object o); 和 E remove(int index); boolean remove(Object o); 和 E remove();

我們來看看它們的差異:

  • 首先看List特有的方法,List表示列表,它支持和索引相關的操作,所以它的添加、移除元素方法裏有支持傳入索引的重載方法,這些是Deque所沒有的。
  • 接着看Deque特有的方法,Deque表示隊列,比較傾向於支持對於首尾兩端的操作,對於空參數的移除方法,默認移除隊頭的元素,這個默認操作是List所沒有的。
  • 接着來看有衝突的方法,對於add(E e),List要求在列表尾部插入元素,Deque要求在隊列尾部插入元素,所以它們兩要求的功能其實是一樣的。對於remove(Object o)兩者都要求移除集合裏面從頭至尾出現的第一個匹配元素,所以它們兩要求的功能其實是一樣的。

好了我們現在可以開始進入添加、移除元素以及其它方法的分析了。

public boolean add(E e)

LinkedList共有兩個重載的add()方法,我們先來看一個參數的方法。

源碼分析

我們把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;
}
    
/**
* 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++;
}

可以看到,add(E e)並沒有實際的功能,內部是用linkLast(E e)來添加元素的。
“link”表示“連接”的意思,linkLast()其實就是向鏈表尾部添加元素。在LinkedList裏面,有若干linkXxx()方法,比如linkLast()、linkFirst()、linkBefore()分別表示向鏈表尾部添加元素、向鏈表頭部添加元素、在鏈表的某個元素前面添加元素。這裏先來看linkLast():

  1. 首先用一個“l”變量保存鏈表尾部的指針,然後創建結點,既然是在鏈表尾部添加結點,那該結點的前驅結點就是原來的表尾結點,後繼結點是null。
  2. 由於新結點就是新的鏈表尾部結點,所以表尾字段“last”指向新的結點。
  3. 判斷“l”是否爲null,其實就是判斷原來的“last”是否爲null,也就是判斷原來的鏈表是否爲空鏈表,也就是判斷此時此刻是不是在添加第一個元素。如果是在添加第一個結點,那表頭表尾的指針都應該指向該新結點,所以會將表頭指針指向新結點。如果不是在添加第一個結點,那就要將原來表尾元素的後繼指針指向新的結點。
  4. 最後將表示鏈表尺寸的字段+1。

過程比較簡單,如果對於各個指針的轉變不太理解,接下來看圖走一遍,就清晰了。

圖示過程

下圖表示的是linkLast(E e)的執行過程,如圖:

LinkedList.linkLast方法

以上就是向鏈表尾部插入元素的圖示過程,左側是向空鏈表尾部插入元素,右側是向非空鏈表尾部插入元素。

源碼小結

向鏈表尾部插入元素的步驟總結如下:

  1. 創建新結點,新結點的前驅指針指向鏈表尾部。
  2. 表尾指針指向新結點。
  3. 如果原來鏈表是空鏈表,則表頭指針也指向新結點;否則,原來的表尾元素的後繼指針指向新結點。

public void add(int index, E e)

接着來看add()的第二個重載方法:在指定的位置添加新元素。

源碼分析

列出該方法及其內部調用的方法如下:

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

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


/**
* Tells if the argument is the index of a valid position for an
* iterator or an add operation.
*/
private boolean isPositionIndex(int index) {
    return index >= 0 && index <= size;
}

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

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

有一個linkLast()由於前文已經分析過了,所以這裏就不貼代碼了重複了。接下來看add(int index, E e)的執行流程:

  1. checkPositionIndex(int index),參數檢查,檢查傳進來的位置是否合法。如果參數不合法,就拋異常,實際的檢查邏輯在isPositionIndex(int index)裏面。
  2. boolean isPositionIndex(int index),如果參數合法,返回true,否則,返回false。合法的範圍是0~size,也就是允許插入元素的最前面的位置是鏈表頭部,最後面的位置是鏈表最後一個結點後面
  3. 如果index等於size,相當於在鏈表尾部插入元素,等價於調用add(E e),所以直接就調用了linkLast(),該方法前文已經分析過了,不再重複。注意一下,如果你向空鏈表插入結點,等價於向表尾插入結點。如果向鏈表尾部以外的位置插入結點,先通過node(int index)獲取指定位置的結點,然後由linkBefor(E e, Node succ)將新結點插入到index所指定的結點前面。
  4. Noede node(int index),根據索引獲取結點。基本思路很簡單,就是遍歷。值得一提的小技巧是它並不總是從頭到尾遍歷,(size >> 1)就是折半,根據索引的位置在鏈表的左半部分還是右半部分選擇是從頭到尾遍歷還是從尾到頭遍歷。
  5. linkBefore(E e, Node succ),將元素e插入到結點succ前面。定義變量“pred”保存succ的前驅結點。爲元素e創建新的結點,新結點的前驅結點是succ的前驅結點,後繼結點是succ。然後,讓succ的前驅指針指向新結點,至此,新結點和succ的相互連接就完成了,接下來是新結點和前面元素的連接。判斷原來的prev結點是否爲空,如果爲空,說明本次插入是在表頭插入結點,也就是新結點就是新的表頭結點,就讓first指向新結點。如果不爲空,就讓原來的prev結點的後繼指針指向新結點。最後將表示集合尺寸的size+1,結束。

過程並不複雜,如果對於各個指針的轉變不太理解,接下來看圖走一遍,就清晰了。

圖示過程

注意:下圖僅表示linkBefore(E e, Node succ)的過程:

LinkedList.linkBefore()方法

以上就是向鏈表指定結點插入元素的圖示過程,左側是向鏈表頭部插入結點,右側是向鏈表頭部以外的位置插入結點。

源碼小結

向鏈表指定位置插入元素的步驟總結如下:

  1. 參數檢查。
  2. 如果指定位置剛好是鏈表尾部,調用linkLast()。
  3. 如果指定位置不是鏈表尾部,調用linkBefore(),進入linkBefore():
    1. 創建新結點,新結點的前驅指針指向“指定位置的結點的前驅結點”,新結點的後繼指針指向“指定位置的結點”。
    2. 指定位置的結點的前驅指針指向新結點。
    3. 如果指定位置的結點剛好是表頭結點,將表頭指針指向新結點。否則,“指定位置的結點的前驅結點的後繼指針”指向新結點。

public boolean remove(Object o)

remove()共有三個重載方法,兩個是來自List接口的,一個是來自Deque接口的,我們先來看來自List的remove(Object o)。

源碼分析

列出該方法及其內部調用的方法如下:

/**
* Removes the first occurrence of the specified element from this list,
* if it is present.  If this list does not contain the element, it is
* unchanged.  More formally, removes the element with the lowest index
* {@code i} such that
* <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
* (if such an element exists).  Returns {@code true} if this list
* contained the specified element (or equivalently, if this list
* changed as a result of the call).
*
* @param o element to be removed from this list, if present
* @return {@code true} if this list contained the specified element
*/
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;
}

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

來看remove(Object o)方法:

  1. remove(Object o)主要做的是根據傳進來的元素找到對應結點,真正移除結點的地方在unlink(Node x)裏面,前面分析過linkXxx()方法,表示“連接”,就是向鏈表裏面添加元素,那麼unlink()表達了相反的意思,就是從鏈表裏面移除元素。
  2. remove(Object o)查找結點的方法也很簡單:從表頭向表尾部遍歷,具體是根據傳入的對象是否爲null分成兩個部分。如果是null,則查找第一個數據域爲null的結點,否則,查找第一個數據域等於傳入參數(由equals()判定)的結點。找到之後使用unlink()移除該結點。
  3. E unlink(Node x),從鏈表裏移除指定結點x。首先分別定義變量指向結點x的前驅結點和後繼結點,接着開始先後處理前驅結點和後繼結點。
    1. 如果前驅結點爲空,說明移除的是表頭結點,此時將表頭指針指向結點x的後繼結點,也就是新的表頭結點。如果前驅結點不爲空,則將前驅結點的後繼指針指向結點x的後繼結點,接着將結點x的前驅指針置爲空。至此完成了對結點x的前驅結點的處理。
    2. 如果後繼結點爲空,說明移除的是表尾結點,此時將表尾指針指向結點x的前驅結點,也就是新的表尾結點。如果後繼結點不爲空,則將後繼結點的前驅指針指向結點x的前驅結點,接着將結點x的後繼指針置爲空。至此完成了對結點x的後繼結點的處理。
  4. 至此,結點x的前驅結點和後繼結點(如果均有)已經連接在一起了,結點x也已經和前後兩個結點斷開連接。接着,釋放結點x指向的元素,size字段-1,將移除的結點裏的元素返回,結束。

過程並不複雜,如果對於各個指針的轉變不太理解,接下來看圖走一遍,就清晰了。

圖示過程

注意:下圖僅表示unlink(Node x)的過程:

LinkedList.unlink方法

上圖分別描述了待移除結點x的四類可能情況對應的移除過程:

  • 結點x既是表頭也是表尾。
  • 結點x是表頭但不是表尾。
  • 結點x不是表頭但是表尾。
  • 結點x既不是表頭也不是表尾。

源碼小結

移除鏈表裏面指定元素的步驟總結如下:

  1. 找到元素所在結點,調用unlink(Node e)移除結點。
  2. 如果目標結點是表頭結點,將表頭指針指向目標結點的後繼結點;否則,將目標結點的“前驅結點的後繼指針”指向目標結點的後繼結點,目標結點的前驅指針置空。
  3. 如果目標結點是表尾結點,將表尾指針指向目標結點的前驅結點;否則,將目標結點的“後繼結點的前驅指針”指向目標結點的前驅結點,目標結點的後繼指針置空。
  4. 目標結點的數據域指針置空(即釋放元素),鏈表長度size-1。

public E remove(int index)

接下來看remove()的另一個重載方法,來自List接口的,移除指定位置的元素。

源碼分析

列出該方法及其內部調用的方法如下:

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

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

由於unlink(Node x)、node(int index)前文已經分析過了,所以這裏就不貼代碼了重複了,來看remove(int index)的執行流程:

  1. remove(int index)的內容很簡單,首先進行參數檢查,然後調用node(int index)找到指定位置的結點,然後調用unlink(Node x)進行移除。
  2. checkElementIndex(int index),檢查索引是否正確,其內部通過isElementIndex(int index)來實現參數檢查,如果參數不合法,拋異常。
  3. boolean isElementIndex(int index),檢查參數所指向的結點是否存在,有效範圍就是0 ~ (size-1)。大家看到這兩個檢查參數的方法裏的代碼段,有沒覺得似曾相識呢,這裏提一下,移除元素時進行參數檢查的checkElementIndex(int index)、isElementIndex(int index)和前文分析添加元素時進行參數檢查的checkPositionIndex(int index)、isPositionIndex(int index)代碼內容幾乎一模一樣,唯一的區別在於添加元素時的有效範圍是0 ~ size,移除元素時的有效範圍是0 ~ (size - 1)。
  4. 找到索引對應的結點,然後移除,這兩個方法前文已經分析過了,不再重複。

源碼小結

由於該方法涉及的關鍵代碼前文已經分析過了,也就不畫流程圖了,直接進行總結吧,從鏈表裏移除指定位置的元素步驟的總結如下:

  1. 參數檢查。
  2. 找到索引對應的結點。
  3. 移除結點。

public E remove()

接下來看remove()的最後一個重載方法,來自Deque接口的,移除隊列頭部的結點,也就是移除鏈表的表頭結點。

源碼分析

列出該方法及其內部調用的方法如下:

/**
* Retrieves and removes the head (first element) of this list.
*
* @return the head of this list
* @throws NoSuchElementException if this list is empty
* @since 1.5
*/
public E remove() {
    return removeFirst();
}

/**
* Removes and returns the first element from this list.
*
* @return the first element from this list
* @throws NoSuchElementException if this list is empty
*/
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

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

我們來看移除表頭結點的流程:

  1. remove()什麼都沒做,直接調用removeFirst()。
  2. removeFirst(),移除鏈表的第一個結點。首先進行參數檢查,如果表頭指針爲null,拋異常,也就是不允許對空鏈表執行移除表頭結點的操作。如果有表頭結點,調用unlinkFirst(Node f)進行移除。
  3. E unlinkFirst(Node f),這是真正進行移除操作的地方。首先使用變量“next”保存表頭結點的後繼結點,然後,將表頭結點的數據域和後繼指針置空,也就是釋放元素以及斷開表頭結點對後繼結點的連接。接着判斷next是否爲空,其實就是判斷被移除的表頭結點是否同時也是表尾結點。如果是,將表尾指針置空;否則,將原表頭結點的後繼結點的前驅指針置空,也就是斷開後繼結點對錶頭結點的連接。

流程不算複雜,如果對於具體的移除流程不清楚,接下來看示意圖吧。

圖示過程

注意:下圖僅表示unlinkFirst(Node f)的過程:

LinkedList.unlinkFirst方法

以上左側表示表頭結點同時也是表尾結點的情況,右側表示表頭結點只是表頭結點不是表尾結點的情況。

源碼小結

移除鏈表頭部結點的remove()流程總結如下:

  1. 參數檢查。
  2. 調用unlinkFirst(Node f)移除表頭結點。
  3. 釋放表頭結點所持有的相關對象,表頭指針指向表頭結點的後繼結點。如果原表頭結點同時也是表尾結點,表尾指針置空;否則,原表頭結點的後繼結點的前驅指針置空。

public boolean addAll(Collection<? extends E> c)

前文其實已經分析完了List接口所有添加單個元素的操作,現在來看添加一堆元素(一個集合)的操作,主要還是爲了說明文章開頭提到的帶參數的構造方法的執行流程

源碼分析

列出帶參數的構造方法及其內部調用的方法如下:

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

/**
* Appends all of the elements in the specified collection to the end of
* this list, in the order that they are returned by the specified
* collection's iterator.  The behavior of this operation is undefined if
* the specified collection is modified while the operation is in
* progress.  (Note that this will occur if the specified collection is
* this list, and it's nonempty.)
*
* @param c collection containing elements to be added to this list
* @return {@code true} if this list changed as a result of the call
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

/**
* Inserts all of the elements in the specified collection into this
* list, starting at the specified position.  Shifts the element
* currently at that position (if any) and any subsequent elements to
* the right (increases their indices).  The new elements will appear
* in the list in the order that they are returned by the
* specified collection's iterator.
*
* @param index index at which to insert the first element
*              from the specified collection
* @param c collection containing elements to be added to this list
* @return {@code true} if this list changed as a result of the call
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
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;
}

我們來看執行流程:

  1. 構造方法沒有實際操作,只是調用了addAll(Collection<? extends E> c)方法。
  2. boolean addAll(Collection<? extends E> c),將傳入的集合裏面所有的元素添加到鏈表尾部。當該方法被構造方法調用,其實就是將傳入的集合裏的元素添加到空鏈表尾部,這個方法也沒有實際的操作,內部只是調用了addAll(int index, Collection<? extends E> c)方法。
  3. boolean addAll(int index, Collection<? extends E> c),將傳入的集合裏面所有的元素添加到鏈表裏面,添加位置由index指定。說明一下,這一堆元素是被插入到原來index所指向的的結點的前面的。
    1. 首先進行參數檢查,先對index進行檢查,checkPositionIndex(int index)前文已經分析過了,不再重複。接着將傳入的集合轉成Object[]類型,並對其進行長度檢查,如果長度爲0,就不存在“添加”的行爲了。
    2. 找到index所指向的結點及其前驅結點,“succ”指向index所指向的結點,“pred”指向其前驅結點。當(index等於size)成立時,index所指向的結點是不存在的,所以這裏其實是根據index所指向的元素是否存在分開處理。同時,只有向空鏈表裏添加元素或者在鏈表尾部添加元素時(index等於size)纔會成立,所以此時的處理辦法是“succ”只能爲null,“pred”指向表尾指針last(如果鏈表是空鏈表,last爲null,剛好滿足)。
    3. 遍歷Object[]裏的元素,逐一爲其生成新結點,從“pred”指向的結點開始,逐一連接起來。我們來看具體過程,生成新結點,新結點的前驅指針指向“pred”結點,後繼指針爲null。判斷(pred等於null)是否成立,其實就是判斷當前鏈表是否爲空鏈表,只有向空鏈表裏面添加第一個元素的時候,這個條件纔會成立,此時應當修改表頭指針,讓first指向新結點。如果(pred等於null)不成立,則讓“pred”的後繼指針指向新結點,至此,新結點和“pred”結點互相連接完成。然後,讓“pred”指向新結點,就可以繼續添加新結點了。
    4. 當新元素添加完了,就要處理最後一個新結點和“succ”結點的關係了。如果(succ == null)成立,說明是在鏈表尾部添加元素,此時讓表尾指針last指向最後一個新結點即可。否則,要讓最後一個新結點和“succ”結點互相連接。

圖示過程

注意:

  • 下圖僅表示addAll(int index, Collection<? extends E> c)的過程。
  • 左上角、右上角、左下角、右下角分別描述了向空鏈表、在表尾、在表頭、向鏈表中部(不是表頭也不是表尾)執行addAll(int index, Collection<? extends E> c)的流程。
  • 假設傳進來的集合裏面只有兩個元素,即只需要創建兩個新結點。

LinkedList.addAll()方法

上圖即addAll(int index, Collection<? extends E> c)的執行流程。

源碼小結

addAll(Collection<? extends E> c)方法裏面只是調用了addAll(int index, Collection<? extends E> c)方法,那隻要對addAll(int index, Collection<? extends E> c)進行總結就可以了:

  1. 參數檢查。
  2. 找到index所在的結點及其前驅結點。
  3. 添加所有元素。
  4. 判斷是否爲表尾部並處理。

Deque接口分析

集合的主要功能就是對數據進行“增刪改查”,在LinkedList裏面,“改”、“查”主要是由前文分析過的“Node node(int index)”來實現的,這裏就不詳細說了,那麼至此,List接口裏常用的“增刪改查”方法在LinkedList裏的具體實現基本分析完了。其實在LinkedList裏面,對於“增刪查”操作是提供了三類方法來實現的:

  • add()、remove()、get()和element()。
  • offer()、poll()、peek()。
  • push()、pop()、peek()。

這裏之所以沒有提到“改”操作是因爲它就只有一個方法:set(int index, E element),而且只在List接口裏定義了,Deque接口裏面沒有定義“改”有關的操作。對於List裏的方法add()、remove()、get()我們已經分析過了,接下來了解剩餘的,對比一下Deque和List接口的差異,以及Deque裏面重複定義的、功能類似的幾個方法的差異。

首先是縱向對比,這三類方法的功能都是對應相同的,即add()、offer()和push()表示“增”,remove()、poll()和pop()表示“刪”,get()、element()和peek()表示“查”。

接着對比第一類和第二類方法,我們先將關注點放在List接口和Deque接口的差異上來進行對比:
add()、remove()、get()主要表示對“列表(即List)”進行的操作,offer()、poll()、peek()主要表示對“隊列(即Deque)”進行的操作。儘管Deque接口也定義了add()、remove()方法,但是,只有List接口裏的add()、remove()方法支持傳入索引作爲參數(即對某個位置上的元素進行操作),Deque接口裏面是完全沒有任何方法支持傳入索引作爲參數的,這是List接口和Deque接口的主要區別。element()方法在兩個接口的對比當中意義不大,下文再說。

繼續對比第一類和第二類方法,但此時拋開List接口,將關注點完全集中在Deque接口自身來進行對比。實際上,第一類和第二類方法裏面,除了get()以外,其餘6個方法在Deque接口裏面全都有定義,那它們有什麼區別:

  • add()、offer()均表示向鏈表尾部添加元素,當添加元素成功時,兩者均返回true。差異在於當添加元素失敗時,offer()總是返回false,如果導致失敗的原因是“容量限制”,add()會拋illegalStateException異常。
  • remove()、poll()均表示刪除並返回鏈表頭部的元素,差異在於如果對空鏈表執行刪除操作,poll()會返回null,而remove()會拋NoSuchElementException異常。
  • element()、peek()均表示獲取(但不刪除,即“查”)鏈表頭部的元素,差異在於如果對空鏈表執行查找操作,peek()會返回null,element()會拋NoSuchElementException異常。

顯然,就Deque接口自身而言,這兩類方法的功能是一樣的,只是對於異常情況的處理不同,add()、remove()、element()以拋異常來表示異常情況,offer()、poll()、peek()以特定返回值來表示異常情況。實際情況是否真的如此呢?不完全是,在LinkedList實際的實現裏面,有一個方法沒有按照Deque裏面的聲明來處理,就是add()。add()方法是沒有拋異常的,至於原因,就源碼的實現來看,筆者猜測在LinkedList裏面add()根本就不存在失敗的情況。

接着對比第二類和第三類方法,它們也是純粹站在Deque接口的角度來看的:
offer()、poll()主要表示對“隊列(即Deque)”進行的操作,push()、pop()主要表示對“棧(即Stack)”進行的操作。儘管LinkedList沒有聲明實現了“棧”相關的接口,但是它確實支持和“棧”有關的操作。由於數據結構對“隊列”、“棧”的定義不同,因此這兩類方法雖然功能對應相同,但是實現卻有一個很大差異,offer()的“增”是在鏈表尾部進行的,poll()的“刪”是在鏈表頭部進行的,而push()和pop()的“增”、“刪”都是在鏈表頭部進行。因爲數據結構對於“隊列”的定義就是隊尾進入,隊頭移出,先進先出;而對於“棧”的定義則是進出都在同一端,後進先出,這是需要注意的一點

總結

如果想要從功能、接口層面比較好的理解LinkedList,應當要先理解好數據結構裏“列表”、“隊列”、“棧”的概念,因爲LinkedList提供了對這三類數據結構進行操作的API。

本文分析了LinkedList所有的字段、構造方法以及部分常用的其它方法,對List和Deque接口裏面定義的“增”、“刪”方法在LinkedList裏的具體實現進行了詳細的分析,並且比較了兩個接口裏面“增”、“刪”操作的異同。對於複雜的方法,都採用了先文字分析源碼、後圖片展示過程的形式,應該說是比較清晰的。從源碼裏可以看出,在鏈表裏面對結點進行“增”、“刪”操作時,需要特別注意處理好空鏈表、在表頭進行操作、在表尾進行操作三類情況

最後,對比了LinkedList裏面三類“增刪查”操作的異同,它們的異同主要體現在數據結構對“列表”、“隊列”、“棧”三者的功能定義方面。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章