1. 程式人生 > 實用技巧 >LinkedList 原始碼分析

LinkedList 原始碼分析

一、概述

本文基於 JDK8

LinkedList 底層通過雙向集合的資料結構實現

  • 記憶體無需連續的空間保證
  • 元素查詢只能是順序的遍歷查詢
  • 針對增刪操作具有更好的效能

LinkedList 可以作為 List 使用,也可以作為佇列和棧使用。支援從集合的頭部,中間,尾部進行新增、刪除等操作。

LinkedList 的繼承與實現的關係圖如下所示。

以下說明摘自 JDK 文件。

  • Iterable 介面:提供迭代器訪問能力,實現此介面允許物件通過 for-each 迴圈語句進行遍歷。
  • Collection 介面:集合層次結構中的根介面。集合中的一組物件稱為元素。一些集合允許重複的元素,而另一些則不允許。有些是有序的,而另一些是無序的。 JDK 不提供此介面的任何直接實現,它提供了更多特定子介面的實現,例如 Set 和 List 。該介面通常用於傳遞集合並在需要最大通用性的地方使用。
  • AbstractCollection 抽象類:此類提供了 Collection 介面的基本實現,以最大程度地減少實現此介面所需的工作。
  • List 介面:Collection 介面的子介面,有序集合(也稱為序列)。使用者通過該介面可以精確控制列表中每個元素的插入位置。使用者可以通過其整數索引(集合中的位置)訪問元素,並在其中搜索元素。
  • AbstractList 抽象類: 此類提供 List 介面的基本實現以最大程度地減少由“隨機訪問”資料儲存(例如陣列) 實現此介面所需的工作。
  • AbstractSequentialList 抽象類:此類提供了 List 介面的基本實現,以最大程度地減少由“順序訪問”資料儲存(例如集合)實現此介面所需的工作。對於隨機訪問資料(例如陣列),應優先使用 AbstractList 類。
  • Queue 介面:設計用於在處理之前容納元素的集合。 除了基本的 Collection 介面操作之外,佇列還提供其他插入,提取和檢查操作。這些方法都以兩種形式存在:一種在操作失敗時引發異常,另一種返回特殊的值(取決於操作, null 或 false)。插入操作的後一種形式是專為容量受限的 Queue 實現而設計的;在大多數實現中,插入操作不會失敗。
  • Deque 介面:支援在兩端插入和刪除元素的線性集合。名稱 Deque 是“雙端佇列”的縮寫,通常發音為“deck”。大多數Deque 的實現都對元素可能包含的元素數量沒有固定的限制,但是此介面支援容量受限的雙端佇列以及沒有固定大小限制的雙端佇列。
  • Cloneable 介面:該標記介面提供例項克隆的的能力。
  • Serializable 介面:該標記介面提供類序列化或反序列化的能力。

二、原始碼分析

2.1 Node 類

Node 是 LinkedList 的私有內部類,是 LinkedList 的核心,是 LinkedList 中用來儲存節點的類,E 符號為泛型,屬性 item 為當前的元素,next 為指向當前節點下一個節點,prev 為指向當前節點上一個節點,是一種雙集合結構。

/**
 * Node 內部類 
 * @param <E>
 */
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;
    }
}

2.2 屬性

/**
 * 序列號
 */
private static final long serialVersionUID = 876323262645176354L;

/**
 * 集合的長度
 */
transient int size = 0;

/**
 * 集合頭節點。
 * Invariant: (first == null && last == null) || (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * 集合尾節點
 * Invariant: (first == null && last == null) || (last.next == null && last.item != null)
 */
transient Node<E> last;

2.3 構造方法

2.3.1 LinkedList()

無參構造器,構造一個空集合。

/**
 * 構造一個空集合
 */
public LinkedList() {
}

2.3.2 LinkedList(Collection<? extends E> c)

構造一個包含指定集合元素的集合,其順序由集合的迭代器返回。

/**
 * 構造一個包含指定集合元素的集合,其順序由集合的迭代器返回。
 *
 * @param  c 指定的集合
 * @throws NullPointerException 集合為空丟擲
 */
public LinkedList(Collection<? extends E> c) {
	// 呼叫無參建構函式進行初始化
    this();
    // 將集合 c 新增進集合
    addAll(c);
}

2.4 主要方法

2.4.1 add(E e)

將指定的元素新增到集合的末尾,該方法和 addLast 方法的作用一樣,主要是通過 linkLast 方法來實現插入到末尾,步驟如圖所示。

/**
 * 將指定的元素新增到集合的末尾
 * 此方法和 addLast 方法的作用一樣
 *
 * @param e 新增的元素
 * @return 返回新增成功
  */
public boolean add(E e) {
    // 呼叫 linkLast 方法插入元素
    linkLast(e);
    return true;
}

/**
 * 將指定的元素新增到集合的末尾
 * 此方法和 add 方法的作用一樣 
 */
public void addLast(E e) {
    linkLast(e);
}

/**
 * 將該元素新增到集合的末尾
 */
void linkLast(E e) {
    // 獲取舊尾節點
    final Node<E> l = last;
    // 構建一個新節點,該新節點的上一個節點指向舊尾節點,下一個節點為 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 將新節點更新到尾節點
    last = newNode;
    
    // 如果舊尾節點為空,則該新節點既是首節點也是尾節點
    if (l == null)
        first = newNode;
    else
        // 舊尾節點不為空的話,將舊尾節點的下一個節點指向新尾節點
        l.next = newNode;
    // 集合長度 +1
    size++;
    // 修改次數 +1,適用於 fail-fast 迭代器
    modCount++;
}

2.4.2 addFirst(E e)

將該元素新增到集合的頭部,主要通過呼叫 linkFirst 方法來實現,步驟如圖所示。

/**
 * 將該元素新增到集合的頭部
 *
 * @param e 新增的元素
 */
public void addFirst(E e) {
    linkFirst(e);
}

/**
 * 將該元素新增到集合的頭部
 */
private void linkFirst(E e) {
    // 獲取集合舊首節點
    final Node<E> f = first;
    // 構建一個新節點,該新節點的下一個節點是舊首節點,上一個節點為 null
    final Node<E> newNode = new Node<>(null, e, f);
    // 將新節點更新到首節點
    first = newNode;
    
    // 如果舊首節點為空,則該新節點既是首節點也是尾節點
    if (f == null)
        last = newNode;
    else
        // 將舊首節點的上一個節點指向新首節點
        f.prev = newNode;
    // 集合長度 +1
    size++;
    // 修改次數 +1
    modCount++;
}


2.4.3 add(int index, E element)

將指定的元素插入集合中的指定位置。 將當前在該位置的元素(如果有的話)和任何後續的元素向右移位。在集合中間插入元素的平均時間複雜度為 O(1),該方式主要通過 node(int index) 方法找到對應位置的節點,再通過 linkBefore(E e, Node succ) 方法進行插入,在集合中間插入的步驟如圖所示。

/**
 * 將指定的元素插入集合中的指定位置
 *
 * @param index 插入的指定位置
 * @param element 插入的元素
 * @throws IndexOutOfBoundsException 下標越界丟擲
 */
public void add(int index, E element) {
    // 檢查 index 是否越界
    checkPositionIndex(index);

    // 如果 index == size,則呼叫 linkLast 方法將該元素插入到最後一個
    if (index == size)
        linkLast(element);
    else
        // 將該節點插入到原來 index 位置的節點之前
        linkBefore(element, node(index));
}

/**
 * 檢查下標是否越界
 *
 * @param index
 */
private void checkPositionIndex(int index) {
    // isPositionIndex 返回 false 則丟擲 IndexOutOfBoundsException
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 檢查下標是否為迭代器或加法操作的有效位置的索引
 */
private boolean isPositionIndex(int index) {
    // 下標小於 0 或 大於 size 返回 false
    return index >= 0 && index <= size;
}

/**
 * 構建 IndexOutOfBoundsException 異常的詳細資訊
 */
private String outOfBoundsMsg(int index) {
    return "Index: "+index+", Size: "+size;
}

/**
 * 在非 null 節點 succ 之前插入元素 e
 * succ 節點為下標 index 所在的節點,將包括該節點和該節點後面的節點往後移
 */
void linkBefore(E e, Node<E> succ) {
    // 獲取 succ 節點的上一個節點
    final Node<E> pred = succ.prev;
    // 構造新節點,該新節點的上一個節點為 pred,下一個節點為 succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 將 succ 的前一個節點指向新節點
    succ.prev = newNode;
    
    // 如果 pred 節點為空,則插入的新節點指向首節點
    if (pred == null)
        first = newNode;
    else
        // pred 節點不為空,則 pred 節點的下一個節點指向新節點
        pred.next = newNode;
    // 集合長度 +1
    size++;
    // 修改次數 +1
    modCount++;
}

2.4.4 remove()

刪除集合的第一個節點,並返回該元素,和 removeFirst 方法的作用一樣,主要通過 unlinkFirst 方法實現刪除頭節點,並返回頭節點的值,刪除節點時將對應的節點值和節點的指向都置為了 null,方便 GC 回收。刪除步驟如圖所示。

/**
 * 刪除集合的第一個節點,並返回該元素
 *
 * @return 返回集合頭節點
 * @throws NoSuchElementException 集合為空丟擲
 */
public E remove() {
    return removeFirst();
}

/**
 * 刪除集合的第一個節點,並返回該元素
 *
 * @return 返回集合頭節點
 * @throws NoSuchElementException 集合為空丟擲
 */
public E removeFirst() {
    // 獲取首節點
    final Node<E> f = first;
    // 沒有首節點丟擲 NoSuchElementException
    if (f == null)
        throw new NoSuchElementException();
    // 刪除首節點
    return unlinkFirst(f);
}

/**
 * 刪除不為 null 的首節點
 */
private E unlinkFirst(Node<E> f) {
    // 獲取舊首節點的值
    final E element = f.item;
    // 獲取舊首節點的下一個節點 next,next 為新首節點
    final Node<E> next = f.next;
    // 將舊首節點的值和下一個節點的指向賦為 null,幫助 GC
    f.item = null;
    f.next = null; 
    // 用新首節點 next 更新首節點 first
    first = next;
    // 如果 next 節點為空,則代表原集合只有一個節點,將尾節點也指向 null
    if (next == null)
        last = null;
    else
        // next 不為空的話,將該新首節點的上一個節點指向 null
        next.prev = null;
    // 集合長度 -1
    size--;
    // 修改次數 +1
    modCount++;
    // 返回刪除的首節點元素
    return element;
}

2.4.5 removeLast()

刪除集合的最後一個節點,並返回該元素,主要通過 unlinkLast方法實現刪除尾節點,並返回尾節點的值,刪除步驟如圖所示。

/**
 * 刪除尾節點並返回該元素
 *
 * @return 返回尾節點
 * @throws NoSuchElementException 集合為空丟擲
 */
public E removeLast() {
    // 獲取尾節點
    final Node<E> l = last;
    // 尾節點為 null 丟擲 NoSuchElementException
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}

/**
 * 刪除不為 null 的尾節點
 */
private E unlinkLast(Node<E> l) {
    // 獲取舊尾節點的值
    final E element = l.item;
    // 獲取舊尾節點的上一個節點 prev,prev 為新尾節點
    final Node<E> prev = l.prev;
    // 將舊尾節點的值和下一個節點的指向置為 null,幫助 GC
    l.item = null;
    l.prev = null;
    // 用新尾節點 prev 更新尾節點 last
    last = prev;
    // 如果新尾節點節點為空,則代表該集合只有一個節點,將首節點指向 null
    if (prev == null)
        first = null;
    else
        // 新尾節點不為空,將新尾節點的下一個節點指向 null
        prev.next = null;
    // 集合長度 -1
    size--;
    // 修改次數 +1
    modCount++;
    // 返回刪除的尾節點的值
    return element;
}

2.4.6 remove(int index)

刪除集合中指定位置的元素。將所有後續元素向前移動,並返回從集合中刪除的元素,先通過 node(int index) 方法獲取指定位置的節點,再通過 unlink(Node x) 方法刪除該節點並返回該節點的值,步驟如圖所示 。

/**
 * 刪除集合中指定位置的元素。將所有後續元素向前移動,並返回從集合中刪除的元素
 *
 * @param index 刪除的位置
 * @return 返回刪除位置的元素
 * @throws IndexOutOfBoundsException 索引越界丟擲
 */
public E remove(int index) {
    // 檢查是否越界
    checkElementIndex(index);
    // 刪除指定下標所在的節點
    return unlink(node(index));
}

/**
 * 刪除指定不為 null 的節點
 */
E unlink(Node<E> x) {
    // 獲取指定節點的值,用於最後返回
    final E element = x.item;
    // 獲取該節點的下一個節點 next
    final Node<E> next = x.next;
    // 獲取該節點的上一個節點 prev
    final Node<E> prev = x.prev;

    // 如果 prev 節點為 null,表示該節點為首節點,將 next 節點指向首節點
    if (prev == null) {
        first = next;
    } else {
        // prev 節點不為 null,則將 prev 的下一個節點指向 next
        prev.next = next;
        // 將該節點的上一個節點置為 null,幫助 GC
        x.prev = null;
    }

    // 如果 next 節點為 null,表示該節點為尾節點,將 prev 節點指向尾節點
    if (next == null) {
        last = prev;
    } else {
        // next 節點不為 null,將 next 的上一個節點指向 prev
        next.prev = prev;
        // 將該節點的下一個節點置為 null,幫助 GC
        x.next = null;
    }
    // 將該節點的值置為 null
    x.item = null;
    // 集合長度 -1
    size--;
    // 修改次數 +1
    modCount++;
    // 返回刪除的元素的值
    return element;
}

2.4.7 get(int index)

返回集合中指定位置的元素,先檢查下標是否越界,再通過 node(index) 方法取到對應下標的節點,該節點的 item 屬性即為對應的值。

/**
 * 返回集合中指定位置的元素
 *
 * @param index 指定位置
 * @return 返回的元素
 * @throws IndexOutOfBoundsException 下標越界丟擲
 */
public E get(int index) {
    // 檢查下標越界
    checkElementIndex(index);
    // node(index) 返回節點
    return node(index).item;
}

/**
 * 返回集合中的第一個元素
 */
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}

/**
 * 返回集合中的最後一個元素
 *
 * @return 
 * @throws NoSuchElementException 集合為空丟擲異常
 */
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}

2.4.8 node(int index)

返回指定元素索引處的(非空)元素,很多方法都會涉及到該方法。

/**
 * 返回指定元素索引處的(非空)元素
 */
Node<E> node(int index) {
	
    // 判斷 index 更接近 0 還是 size 來決定從哪邊遍歷
    if (index < (size >> 1)) {
        // 從首節點遍歷
        // 獲取首節點
        Node<E> x = first;
        // 從首節點往後遍歷 index 次,獲取到 index 下標所在的節點
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        // 從尾節點遍歷
        // 獲取尾節點
        Node<E> x = last;
        // 從首節點往前遍歷 size-index-1 次,獲取到 index 下標所在的節點
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

2.4.9 offer(E e)

將指定的元素新增到集合的尾部,該方法的作用和 add(E e) ,addLast(E e) 一樣。當使用容量受限的雙端佇列時,此方法通常比 add 方法更可取,當超出佇列容量時,該方法會返回 false,而 add 方法則會丟擲異常。

/**
 * 將指定的元素新增到集合的尾部
 * @param e 需要插入的元素
 * @return 返回是否插入成功
 */
public boolean offer(E e) {
    return add(e);
}

/**
 * 插入特定元素到集合末尾,該方法和 addLast 作用一樣
 *
 * @param e 插入的元素
 * @return 返回 true
 */
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

/**
 * 將元素插入到集合的頭部
 *
 * @param e 插入的元素
 * @return 返回 true
 */
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

2.4.10 poll()

刪除集合的頭節點,該方法的作用和 remove()一樣,不過當兩個方法對空集合使用時,remove() 方法會丟擲異常,而 poll() 方法會返回 null。

pollFirst() 方法和 poll() 方法的作用一樣,當集合為空時返回 null。

pollLast() 方法和 removeLast() 方法的作用一樣,不過當集合為空時,pollLast() 方法回 null,removeLast() 方法丟擲異常。

/**
 * 刪除集合的頭節點
 *
 * @return 返回 null 或頭節點元素
 */
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

/**
 * 刪除集合頭節點
 *
 * @return 返回 null 或集合的一個元素
 */
public E pollFirst() {
    // 獲取首節點
    final Node<E> f = first;
    // 如果該集合沒有元素則返回 null,否則刪除首節點
    return (f == null) ? null : unlinkFirst(f);
}

/**
 * 檢刪除連結串列的最後一個元素
 *
 * @return 返回 null 或集合最後一個元素
 */
public E pollLast() {
    // 獲取尾節點
    final Node<E> l = last;
    // 如果該集合沒有元素則返回 null,否則刪除尾節點
    return (l == null) ? null : unlinkLast(l);
}

2.4.11 peek()

peek() 方法的作用和 getFirst() 方法一樣,不過當集合為空時,peek() 方法返回 null,而 getFirt() 方法丟擲異常。

peekFirst() 方法的作用和 peek() 一樣,peekLast() 方法的作用和 removeLast() 方法一樣,不過該方法遇到空集合也是返回 null。

/**
 * 返回集合的第一個元素
 *
 * @return 
 */
public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

/**
 * 返回集合的第一個元素,作用和 peek() 一樣
 *
 * @return
 */
public E peekFirst() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

/**
 * 返回集合的最後一個元素
 *
 * @return 
 */
public E peekLast() {
    final Node<E> l = last;
    return (l == null) ? null : l.item;
}

三、總結

  • LinkedList 底層是基於連結串列的,查詢節點的平均時間複雜度是 O(n),首尾增加和刪除節點的時間複雜度是 O(1)。
  • LinkedList 適合讀少寫多的情況,ArrayList 適合讀多寫少的情況。
  • LinkedList 作為佇列使用時,可以通過 offer/poll/peek 來代替 add/remove/get 等方法, 這些方法在遇到空集合或佇列容量滿的情況不會丟擲異常。