List介面下的集合原始碼分析——LinkedList

weixin_33866037發表於2018-05-01

原始碼版本JDK1.8

今天帶來的是List的另一種實現——LinkedList,這是一種基於雙向連結串列實現的列表。接下來讓我們通過原始碼來分析一下它吧。

關於原始碼中的一些小改動

在JDK1.6及之前,LinkedList底層是一個雙向迴圈連結串列,容器中的元素都是靜態內部類Entry的物件,列表中必有一個空頭結點;
在JDK1.7及之後,LinkedList底層是一個雙向非迴圈連結串列,容器中的元素都是靜態內部類Node的物件。
基於這些小差別,筆者分享下自己的見解:

  • 使用非迴圈連結串列後,可以少一個空的頭結點,在頭尾加入元素時可以少一些引用操作(對於迴圈連結串列來說,由於首尾相連,還是需要處理兩頭的前驅和後繼引用。而非迴圈連結串列只需要處理一邊first.previous/last.next,所以理論上非迴圈連結串列更高效。恰恰在兩頭(鏈頭/鏈尾) 操作是最普遍的)
  • 對於Entry改變成Node,本質上是沒有差別的。可能大家對Entry的印象是Map中實現的一個內部類,用來儲存鍵值對<key,value>,而在LinkedList中是要儲存<previous,item,next>,不便於凸顯Entry儲存鍵值對的特性吧,容易造成混淆。(這只是個人的猜測,若有不同見解可以交流)
    補充:不論是Entry還是Node,都是外部類LinkedList實現的一個靜態內部類,這麼做是把一個類相關的型別放到內部,提高類的高內聚,而且通常情況下只有該外部類會呼叫其內部類,如果把Entry或者Node放到外部,明顯就提高了耦合性,對於其他集合型別的內部實現來說都是不利的。
    再有一個,內部類會隨著外部類的載入而產生。
    傳送門:關於靜態內部類
    11151952-654f97d7b7879d53.png
    雙向迴圈連結串列結構

    11151952-fac8e8860b0fdaf3.png
    雙向非迴圈連結串列結構

一、LinkedList概述

在原始碼中對LinkedList是這麼描述的:

  • 雙向連結串列實現 ListDeque介面。實現所有可選的列表操作,並允許所有元素null。
  • 所有操作的執行方式與雙向連結串列都是一樣的。索引到列表中的操作將從開始或結束遍歷列表,無論哪個更接近指定的索引。
  • 此實現未同步。
    *此類的 iterator和listIterator方法返回的迭代器:如果在建立迭代器之後的任何時間對結構進行修改,除了通過迭代器自己的remove}或{@code add方法,迭代器將丟擲一個ConcurrentModificationException。因此,面對併發修改,迭代器快速而乾淨地失敗,而不是在將來的未確定時間冒任意的,非確定性行為的風險。

二、LinkedList的繼承、實現關係

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  • 繼承自AbstractSequentialList,而AbstractSequentialList父類為AbstractListAbstractSequentialList 實現了get(int index)、set(int index, E element)、add(int index, E element) 和 remove(int index)這些骨幹性函式。
  • 實現List介面,能對它進行佇列操作。
  • 實現Deque介面,而DequeQueue的子介面。Queue是一種佇列形式,而Deque則是雙向佇列,它支援從兩個端點方向檢索和插入元素,因此Deque既可以支援LIFO形式也可以支援LIFO形式。Deque介面是一種比StackVector更為豐富的抽象資料形式,因為它同時實現了以上兩者。傳送門:Deque雙端佇列
  • 實現了Cloneable介面,即覆蓋了函式clone(),能克隆。
  • 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸
    11151952-9917b4af5556c213.jpg
    繼承實現關係.jpg

三、LinkedList屬性宣告及建構函式

transient int size = 0;
transient Node<E> first;//指向第一個節點的指標
transient Node<E> last;//指向最後一個節點的指標
//構造一個空列表
public LinkedList() {
    }
//按照集合的迭代器返回的順序構造包含指定集合的​​元素的列表
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;
    }

帶Collection值的構造方法的執行邏輯:
1)使用this()呼叫預設的無參建構函式;
2)呼叫addAll()方法,傳入當前的節點個數size,此時size為0,並將collection物件傳遞進去;
3)檢查index有沒有陣列越界的嫌疑;
4)將collection轉換成陣列物件a;
5)迴圈遍歷a陣列,然後將a陣列裡面的元素建立成擁有前後連線的節點,然後一個個按照順序連起來;
6)修改當前的節點個數size的值;
7)操作次數modCount自增1。

四、LinkedList的方法

(一)新增元素

—在頭部新增元素

//在此列表的開頭插入指定的元素
public void addFirst(E e) {
        linkFirst(e);
    }

private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

linkFirst(E e)是一個私有方法,所以無法在外部程式中呼叫(當然,這是一般情況,你可以通過反射上面的還是能呼叫到的)。
linkFirst(E e)首先構造一個變數結點f = first,再 new一個newNode(為要新增進來的節點),其前驅引用previous為null,後繼引用為f,再另頭結點指向新的節點newNode。
判斷,如果f == null,即列表為空,則頭尾節點指向同一個節點newNode;如果不為空,原來頭結點的前驅引用指向新節點newNode。
—在尾部新增元素

//將指定的元素追加到此列表的末尾
public void addLast(E e) {
        linkLast(e);
    }
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++;
    }

原理與在頭部新增元素類似,可參照上面進行解讀。
—在任意位置新增元素

// 在此列表中指定的位置插入指定的元素。將當前在該位置的元素(如果有)和任何後續元素向右移(將一個新增到它們的索引)
public void add(int index, E element) {
        checkPositionIndex(index);
        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
//在非空節點succ之前插入元素e
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++;
    }

從原始碼中看出,若索引index==size,便直接在尾部新增元素;若不是,則呼叫linkBefore(E e, Node<E> succ)函式。
linkBefore(E e, Node<E> succ)首先構造一個變數結點pred = succ.prev,再 new一個newNode(為要新增進來的節點),其前驅引用previous為pred ,後繼引用為succ,再另結點succ的前驅指向新的節點newNode。
判斷,如果pred == null,即列表為空,則頭尾節點指向同一個節點newNode;如果不為空,原來pred結點的後繼引用指向新節點newNode。

(二)檢視元素

檢視元素使用get方法。getFirst()、getLast()分別返回頭結點和尾節點。下面主要看看返回指定索引的方法get(int index)。

//返回此列表中指定位置的元素
public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
 private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }
//返回指定元素索引處的(非空)節點
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;
        }
    }

get(int index)首先判斷給定索引是否存在,若存在執行node(index).item;其中item為元素的內容。
node(int index)方法返回的是一個節點Node<E>,程式碼中使用了類似二分法的查詢方法來遍歷元素。若index < (size >> 1),即索引在前半部分,則從前向後依次查詢;否則索引就在後半部分,從後向前依次查詢。
此段程式碼能夠有效的提高遍歷效率,也反映了雙向連結串列的優點——雙向連結串列增加了一點點的空間消耗(每個Node裡面還要維護它的前置Node的引用),同時也增加了一定的程式設計複雜度,卻大大提升了遍歷效率(體現在可以雙向遍歷)。

(三)刪除元素

removeFirst(),removeLast()分別用來刪除頭結點和尾結點,public E remove()方法刪除的也是列表的第一個元素,但是列表為空時使用不會丟擲異常(removeFirst()會丟擲異常)。
ArrayList一樣,LinkedList支援按元素刪除和按下標刪除,下面我們主要介紹public E remove(int index),public boolean remove(Object o)

//刪除此列表中指定位置的元素。將任何後續元素向左移(從它們的索引中減去一個)。返回從列表中刪除的元素
public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
//取消連結非空節點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(int index):
首先通過遍歷node(index)得到指定索引的節點,後通過unlink()方法進行刪除。
(1)x.prev = null;//前驅設定為null
(2)x.next = null;//後繼設定為null
(3)x.item = null;//內容設定為null
至此節點x為空節點,最後交給虛擬機器gc完成回收,刪除操作結束。

//從列表中刪除指定元素的第一次出現(如果存在)。如果此列表不包含元素,則不會更改。
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):
不論元素內容為空還是不為空,均通過節點的遍歷,依次查詢,若找到與指定內容一致的節點則刪除並返回。
注意:該方法從列表中刪除第一次出現的指定元素。


LinkedList的方法比較簡單,沒有擴容環節,翻閱原始碼基本能懂,不存在什麼大問題。由於LinkedList實現了Deque介面,該介面比List提供了更多的方法,包括 offer(),peek(),poll()等。

//檢索,但不刪除此列表的頭(第一個元素)
public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
//檢索並刪除此列表的頭(第一個元素)
public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
//檢索但不刪除此列表的第一個元素,如果此列表為空,則返回null
public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }
//檢索但不刪除此列表的最後一個元素,如果此列表為空,則返回null
public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }
//檢索並刪除此列表的第一個元素,如果此列表為空,則返回null
public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
//檢索並刪除此列表的最後一個元素,如果此列表為空,則返回null
public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }
//將指定的元素新增為此列表的尾部(最後一個元素)
public boolean offer(E e) {
        return add(e);
    }
//在此列表的前面插入指定的元素
public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }
//在此列表的結尾插入指定的元素
public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
//將元素推送到此列表所表示的堆疊。換句話說,將元素插入此列表的前面,此方法等效於addFirst
public void push(E e) {
        addFirst(e);
    }
//此列表所表示的堆疊中彈出一個元素。換句話說,刪除並返回此列表的第一個元素,此方法等效於removeFirst()
public E pop() {
        return removeFirst();
    }

五、ArrayListLinkedList的區別

(一)從插入、刪除元素分析

對於兩者的插入、刪除操作不能片面的蓋棺定論,應視情況而定,下面以插入操作做分析(刪除操作的分析類似)
順序插入:

  • ArrayList在不擴容的情況下順序插入速度較快,因為在構造ArrayList之前已經分配好空間,順序插入元素只是往指定記憶體空間補個元素;在需要擴容的情況下,ArrayList的順序插入則顯得比較慢,因為底層需要執行copy操作,既耗時又耗空間。
  • LinkedList順序新增元素會教慢點,因為每新增一個元素都要新new一個節點物件,並且還有執行其他的引用賦值操作。

中間插入:

  • ArrayList在執行中間插入的過程中耗時的是索引後面的元素copy移動,若果插入的位置越靠前則越慢,反之越快。
  • LinkedList在任何位置插入的效率基本上是一致的,耗時的部分主要是定位索引(定址),賦值部分只需修改引用。

綜合以上所述:
(1)LinkedList做插入、刪除的時候,慢在定址,快在只需要改變前後Node的引用地址。
(2)ArrayList做插入、刪除的時候,慢在陣列元素的批量copy,快在定址。

所以,如果待插入、刪除的元素是在資料結構的前半段尤其是非常靠前的位置的時候,LinkedList的效率將大大快過ArrayList,因為ArrayList將批量copy大量的元素;越往後,對於LinkedList來說,因為它是雙向連結串列,所以在第2個元素後面插入一個資料和在倒數第2個元素後面插入一個元素在效率上基本沒有差別,但是ArrayList由於要批量copy的元素越來越少,操作速度必然追上乃至超過LinkedList

(二)從遍歷列表分析

未完待續。。。

相關文章