LinkedList與Queue原始碼分析

GitLqr發表於2017-11-08

java中的資料結構原始碼解析的系列文章:
ArrayList原始碼分析
LinkedList與Queue原始碼分析

一、簡述

上篇已經分析了基於陣列實現資料儲存的ArrayList(線性表),而本篇的主角是LinkedList,這個使用了連結串列實現資料儲存的集合,它的增、刪、查、改方式又會是怎樣的呢?下面就開始對LinkedList的原始碼進行分析吧。

二、分析

List

在分析LinkedList之前,還是先瞄一眼List介面,雖然前篇已經看過一遍了,但為了明確下文的分析方向,還是先把List介面中的幾個增刪改查方法再列一次。

public interface List<E> extends Collection<E> {
    boolean add(E e);
    void add(int index, E element);
    boolean remove(Object o);
    E remove(int index);
    E set(int index, E element);
    E get(int index);
    ...
}複製程式碼

LinkedList

1、成員變數

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
    transient int size = 0;
    transient Node<E> first;
    transient Node<E> last;
    ...
}複製程式碼
  • size:陣列元素個數
  • first:頭節點
  • last:尾節點

LinkedList的成員變數很少,就上面那3個,其中first和last都是Node型別(即節點型別),用來表示連結串列的頭和尾,這跟ArrayList就存在著本質的區別了。

要注意:

first和last僅僅只是節點而已,跟資料元素沒有關係,可以認為就是2個額外的"指標",分別指著連結串列的頭和尾。

2、建構函式

1)LinkedList

public LinkedList() {
}複製程式碼

LinkedList的建構函式有2個,以平時最常用的建構函式為例,發現該建構函式什麼事都沒做。

2)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是雙向連結串列。

為什麼Node這個類是靜態的?答案是:這跟記憶體洩露有關,Node類是在LinkedList類中的,也就是一個內部類,若不使用static修飾,那麼Node就是一個普通的內部類,在java中,一個普通內部類在例項化之後,預設會持有外部類的引用,這就有可能造成記憶體洩露。但使用static修飾過的內部類(稱為靜態內部類),就不會有這種問題,在Android中,有很多這樣的情況,如Handler的使用。好像扯遠了~

好了,那下面就看看LinkedList是怎麼進行增、刪、改、查的。

3、增

1)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++;
}複製程式碼

因為LinkedList是連結串列結構,所以每新增一個元素就是讓這個元素連結到連結串列的尾部。

add(E e)的核心是linkLast()方法,它對元素進行了真正新增操作,分為以下幾個步驟:

  1. 先讓此時集合中的尾節點(即last"指標"指向的節點)賦給變數 l 。
  2. 然後,建立一個新節點,結合Node的建構函式,我們可以知道,在建立新節點(newNode)的同時,newNode的prev指向了l(即之前集合中的尾節點),變數 l 就是newNode的前驅節點了,newNode的後繼節點為null。
  3. 再將last指向newNode,也就是說newNode成為該連結串列新的末尾節點。
  4. 接著,判斷變數 l 是否為null,若是null,說明之前集合中沒有元素(此時newNode是集合中唯一一個元素),則將first指向newNode,也就是說此時的newNode既是頭節點又是尾節點(要知道,這時newNode中的prev和next均是null,但被first和last同時指向);

    若變數 l 不是null,說明之前集合中已經存在了至少一個元素,則讓之前集合中的尾節點(即變數 l )的next指向newNode。(結合步驟2,此時的newNode與newNode的前驅節點 l 已經是相互指向了)
  5. 最後,跟ArrayList一樣,讓記錄集合長度的size加1。

通過對add(E e)方法的分析,我們也知道了,原來LinkedList中的元素就是一個個的節點(Node),而真正的資料則存放在Node之中(資料被Node的item所引用)。

2)add(int index, E element)

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

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}複製程式碼

該add方法將新增集合元素分為2種情況,一種是在集合尾部新增,另一種是在集合中間或頭部新增,因為第一種情況也是呼叫linkLast()方法,這裡不再囉嗦,我們看看第二種情況,分析linkBefore(E e, 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++;
}複製程式碼

往LinkedList集合中間或頭部新增元素分為以下幾個步驟:

  1. 先呼叫node(int index)方法得到指定位置的元素節點,也就是linkBefore()方法中的形參 succ。
  2. 然後,通過succ.prevt得到succ的前一個元素pred。(此時拿到了第index個元素succ,和第index-1個元素pred)
  3. 再建立一個新節點newNode,newNode的prev指向了pred,newNode的next指向了succ。(即newNode往succ和pred的中間插入,並單向與它們分別建立聯絡,eg:pred ← newNode → succ
  4. 再讓succ的prev指向newNode。(succ與跟newNode建立聯絡了,此時succ與newNode是雙向關聯,eg:pred ← newNode ⇋ succ)。
  5. 接著,判斷pred是否為null,若是null,說明之前succ是集合中的第一個元素(即index值為0),現在newNode跑到了succ前面,所以只需要將first指向newNode(eg:first ⇌ newNode ⇋ succ);

    若pred不為null,則將pred的next指向newNode。(這時pred也主動與newNode建立聯絡了,此時pred與newNode也是雙向關聯,eg:pred ⇌ newNode ⇋ succ
  6. 最後,讓記錄集合長度的size加1。

對於連結串列的操作還是有些複雜的,特別是這種雙向連結串列,不過仔細理解下,也不是什麼問題(看不懂的可以邊看步驟邊動手畫一畫)。到這裡,對於LinkedList的第一個新增方法就分析完了。

下面是對node(int index)方法的分析:

這也是LinkedList獲取元素的核心方法,相當重要,因為後面會出現很多次,這裡就順帶先分析一下了。

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;
    }
}複製程式碼

  細看node(int index)方法中的程式碼邏輯,可以看到,它是通過遍歷的方式,將集合中的元素一個個拿出來,再通過該元素的prev或next拿到下一個遍歷的元素,經過index次迴圈後,最終才拿到了index對應的元素。

  跟ArrayList相比,因為ArrayList底層是陣列實現,擁有下標這個特性,在獲取元素時,不需要對集合進行遍歷,所以查詢某個元素會特別快(在資料量特別多的情況下,ArrayList和LinkedList在效率上的差別就相當明顯了)。

  不過,LinkedList對元素的獲取還是做了一定優化的,它對index與集合長度的一半做比較,來確定是在集合的前半段還是後半段進行查詢。

4、刪

1)remove(int index)

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
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(int index)這個方法中,先通過index和node(int index)拿到了要被刪除的元素x,然後呼叫了unlink(Node x)方法將其刪除,自然,LinkedList刪除元素的核心方法就是unlink(Node x),刪除操作分以下幾個步驟:

  1. 通過要刪除的元素x拿到它的前驅節點prev和後繼節點next。

  2. 若前驅節點prev為null,說明x是集合中的首個元素,直接將first指向後繼節點next即可;


    若不為null,則讓前驅節點prev的next指向後繼節點next,再將x的prev置空。(這時prev與x的關聯就解除了,並與next建立了聯絡)。


  3. 若後繼節點next為null,說明x是集合中的最後一個元素,直接將last指向前驅節點prev即可;(下圖分別對應步驟2中的兩種情況)


    若不為null,則讓後繼節點next的prev指向前驅節點prev,再將x的next置空。(這時next與x的關聯就解除了,並與prev建立了聯絡)。

  4. 最後,讓記錄集合長度的size減1。

2)remove(Object o)

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;
}複製程式碼

remove(Object o)這個刪除元素的方法的形參o是資料本身,而不是LinkedList集合中的元素(節點),所以需要先通過節點遍歷的方式,找到o資料對應的元素,然後再呼叫unlink(Node x)方法將其刪除,關於unlink(Node x)的分析在第一個刪除方法中已經提到了,可往回再看看。

5、查 & 改

LinkedList集合對資料的獲取與修改均通過node(int index)方法來執行往後的操作,關於node(int index)方法的分析也已經在第一個新增方法的時候已經提過,這裡也就不再囉嗦了。

1)set(int index, E element)

public E set(int index, E element) {
    checkElementIndex(index);
    Node<E> x = node(index);
    E oldVal = x.item;
    x.item = element;
    return oldVal;
}複製程式碼

2)get(int index)

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}複製程式碼

三、佇列Queue

這裡要順帶分析下java中的佇列實現,why?因為java中佇列的實現就是LinkedList,你可能會疑問,佇列的英文是Queue,在java中也有對應的介面,怎麼會跟LinkedList扯上關係呢?因為LinkedList實現了佇列:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    ...
}複製程式碼

程式碼中的Deque是Queue的一個子介面,它繼承了Queue:

public interface Deque<E> extends Queue<E> {...}複製程式碼

從這兩者的關係,不難得出,佇列的實現方式也是連結串列。下面先來看看Queue的介面宣告:

1、Queue

我們知道,佇列是先進先出的,新增元素只能從隊尾新增,刪除元素只能從隊頭刪除,Queue中的方法就體現了這種特性。

public interface Queue<E> extends Collection<E> {
    boolean offer(E e);
    E poll();
    E peek();
    ...
}複製程式碼
  • offer():新增隊尾元素
  • poll():刪除隊頭元素
  • peek():獲取隊頭元素

從上面這幾個方法出發,來看看LinkedList是如何實現的。

2、LinkedList對Queue的實現

1)增

public boolean offer(E e) {
    return add(e);
}複製程式碼

可以看到,在LinkedList中,佇列的offer(E e)方法實際上是呼叫了LinkedList的add(E e),add(E e)已經在最前面分析過了,就是在連結串列的尾部新增一個元素~

2)刪

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(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;
}複製程式碼

poll()方法先拿到隊頭元素 f ,若 f 不為null,就呼叫unlinkFirst(Node f)其刪除。unlinkFirst(Node f)在實現上跟unlink(Node x)差不多且相對簡單,這裡不做過多說明。

3)查

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}複製程式碼

peek()先通過first拿到隊頭元素,然後取出元素中的資料實體返回而已。

四、總結

  1. LinkedList是基於連結串列實現的,並且是雙向連結串列。
  2. LinkedList中的元素就是一個個的節點,而真正的資料則存放在Node之中。
  3. LinkedList通過遍歷的方式獲取集合中的元素,效率比ArrayList低。
  4. Queue佇列的實現方式也是連結串列,java中,LinkedList是Queue的實現。

相關文章