LinkedList與Queue源碼分析

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的實現。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章