搞懂 Java LinkedList 原始碼

像一隻狗發表於2018-03-31

LinkedList 原始碼分析

由於最近工作有點忙,進行了 APP 的部分優化,期間也學習了很多有關於佈局優化和其他效能優化的知識,但是仍然覺得不太成體系,期待能有更多的優質的效能優化實戰文章能夠湧現出來,以便於大家一起交流學習。

週末有時間把手頭的工作放一放,來繼續進行 Java 集合原始碼的學習。今天來學習下 「LinkedList」的原始碼。

  1. LinkedList 的概述
  2. LinkedList 的構造方法
  3. LinkedList 的增刪改查。
  4. LinkedList 作為佇列(Queue)使用的時候增刪改查。
  5. LinkedList 的遍歷方法

LinkedList 的概述

先來看下 LinkedList 的繼承體系圖,這裡悄悄告訴大家一個方法在學習原始碼的時候如何檢視一個類的繼承體系的方法,第一步開啟 IntelliJ IDEA 找到你要檢視的類 ,第二步點選右鍵,選擇 Diagrams 選擇二級選單的任意一項,就可以得到下面這樣一個體系圖,還有好多方便的操作,大家可以通過這篇文章來了解下 使用IntelliJ IDEA檢視類的繼承關係圖形

搞懂 Java LinkedList 原始碼

圖中藍色實線箭頭是指繼承關係 ,綠色虛線箭頭是指介面實現關係。

  1. LinkedList 繼承自 AbstrackSequentialList 並實現了 List 介面以及 Deque 雙向佇列介面,因此 LinkedList 不但擁有 List 相關的操作方法,也有佇列的相關操作方法。

  2. LinkedListArrayList 一樣實現了序列化介面 SerializableCloneable 介面使其擁有了序列化和克隆的特性。

LinkedList 一些主要特性:

  1. LinkedList 集合底層實現的資料結構為雙向連結串列
  2. LinkedList 集合中元素允許為 null
  3. LinkedList 允許存入重複的資料
  4. LinkedList 中元素存放順序為存入順序。
  5. LinkedList 是非執行緒安全的,如果想保證執行緒安全的前提下操作 LinkedList,可以使用 List list = Collections.synchronizedList(new LinkedList(...)); 來生成一個執行緒安全的 LinkedList

連結串列是一種不同於陣列的資料結構,雙向連結串列是連結串列的一種子資料結構,它具有以下的特點:

每個節點上有三個欄位:當前節點的資料欄位(data),指向上一個節點的欄位(prev),和指向下一個節點的欄位(next)。

LLink Data RLink

LinkedList 雙向連結串列實現及成員變數

概述上說了雙向連結串列的特點,而 LinkedList 又繼承自 Deque 這個雙連結串列介面,在介紹 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 主要成員變數有下邊三個:

//LinkedList 中的節點個數
transient int size = 0;

//LinkedList 連結串列的第一個節點
transient Node<E> first;

//LinkedList 連結串列的最後一個節點
transient Node<E> last;

複製程式碼

之所以 LinkedList 要儲存連結串列的第一個節點和最後一個節點是因為,我們都知道,連結串列資料結構相對於陣列結構,有點在於增刪,缺點在於查詢。如果我們儲存了LinkedList 的頭尾兩端,當我們需要以索引來查詢節點的時候,我們可以根據 indexsize/2 的大小,來決定從頭查詢還是從尾部查詢,這也算是一定程度上彌補了單連結串列資料結構的缺點。

LinkedList 的建構函式

LinkedList 有兩個建構函式:

/**
 * 空引數的構造由於生成一個空連結串列 first = last = null
 */
 public LinkedList() {
 }

/**
 * 傳入一個集合類,來構造一個具有一定元素的 LinkedList 集合
 * @param  c  其內部的元素將按順序作為 LinkedList 節點
 * @throws NullPointerException 如果 引數 collection 為空將丟擲空指標異常
 */
public LinkedList(Collection<? extends E> c) {
   this();
   addAll(c);
}
複製程式碼

帶引數的構造方法,呼叫 addAll(c) 這個方法,實際上這方法呼叫了 addAll(size, c) 方法,在外部單獨呼叫時,將指定集合的元素作為節點,新增到 LinkedList 連結串列尾部: 而 addAll(size, c) 可以將集合元素插入到指定索引節點。

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
複製程式碼
/**
 * 在 index 節點前插入包含所有 c 集合元素的節點。
 * 返回值表示是否成功新增了對應的元素.
 */
public boolean addAll(int index, Collection<? extends E> c) {
   // 檢視索引是否滿足 0 =< index =< size 的要求
   checkPositionIndex(index);
    // 呼叫對應 Collection 實現類的 toArray 方法將集合轉為陣列
   Object[] a = c.toArray();
   //檢查陣列長度,如果為 0 則直接返回 false 表示沒有新增任何元素
   int numNew = a.length;
   if (numNew == 0)
       return false;
   // 儲存 index 當前的節點為 succ,當前節點的上一個節點為 pred
   Node<E> pred, succ;
   // 如果 index = size 表示在連結串列尾部插入
   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);
       //如果 pred 為空表示 LinkedList 集合中還沒有元素
       //生成的第一個節點將作為頭節點 賦值給 first 成員變數
       if (pred == null)
           first = newNode;
       else
           pred.next = newNode;
       pred = newNode;
   }
   // 如果 index 位置的元素為 null 則遍歷陣列後 pred 所指向的節點即為新連結串列的末節點,賦值給 last 成員變數
   if (succ == null) {
       last = pred;
   } else {
       // 否則將 pred 的 next 索引指向 succ ,succ 的 prev 索引指向 pred
       pred.next = succ;
       succ.prev = pred;
   }
   // 更新當前連結串列的長度 size 並返回 true 表示新增成功
   size += numNew;
   modCount++;
   return true;
}
複製程式碼

經過上邊的程式碼註釋可以瞭解到,LinkedList 批量新增節點的方法實現了。大體分下面幾個步驟:

  1. 檢查索引值是否合法,不合法將丟擲角標越界異常
  2. 儲存 index 位置的節點,和 index-1 位置的節點,對於單連結串列熟悉的同學一定清楚對於連結串列的增刪操作都需要兩個指標變數完成,可參考:搞懂單連結串列面試題 來深入理解下。
  3. 將引數集合轉化為陣列,迴圈將陣列中的元素封裝為節點新增到連結串列中。
  4. 更新連結串列長度並返回新增 true 表示新增成功。

對於 checkPositionIndex方法這裡想順帶分析了,LinkedList 中有兩個方法用於檢查角標越界,內部實現一樣,都是通過判斷 index >= 0 && index < size 是否滿足條件。

private String outOfBoundsMsg(int index) {
   return "Index: "+index+", Size: "+size;
}

private void checkElementIndex(int index) {
   if (!isElementIndex(index))
       throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private void checkPositionIndex(int index) {
   if (!isPositionIndex(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;
}

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

LinkedList 的增刪改查

LinkedList 新增節點的方法

LinkedList 作為連結串列資料結構的實現,不同於陣列,它可以方便的在頭尾插入一個節點,而 add 方法預設在連結串列尾部新增節點:

/**
 * Inserts the specified element at the beginning of this list.
 *
 * @param e the element to add
 */
 public void addFirst(E e) {
    linkFirst(e);
 }

/**
 * Appends the specified element to the end of this list.
 *
 * <p>This method is equivalent to {@link #add}.
 *
 * @param e the element to add
 */
 public void addLast(E e) {
    linkLast(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;
 }
複製程式碼

上述英文太過簡單不翻譯了,我們可以看到 add 方法是有返回值的,這個可以注意下。看來這一系方法都呼叫用了 linkXXX 方法,

 /**
  * 新增一個元素在連結串列的頭節點位置
  */
private void linkFirst(E e) {
   // 新增元素之前的頭節點
   final Node<E> f = first;
   //以新增的元素為節點值構建新的頭節點 並將 next 指標指向 之前的頭節點
   final Node<E> newNode = new Node<>(null, e, f);
   // first 索引指向將新的節點
   first = newNode;
   // 如果新增之前連結串列空則新的節點也作為未節點
   if (f == null)
       last = newNode;
   else
       f.prev = newNode;//否則之前頭節點的 prev 指標指向新節點
   size++;
   modCount++;//運算元++
}

/**
 * 在連結串列末尾新增一個節點
 */
 void linkLast(E e) {
   final Node<E> l = last;//儲存之前的未節點
   //構建新的未節點,並將新節點 prev 指標指向 之前的未節點
   final Node<E> newNode = new Node<>(l, e, null);
   //last 索引指向末節點
   last = newNode;
   if (l == null)//如果之前連結串列為空則新節點也作為頭節點
       first = newNode;
   else//否則將之前的未節點的 next 指標指向新節點
       l.next = newNode;
   size++;
   modCount++;//運算元++
}
複製程式碼

除了上述幾種新增元素的方法,以及之前在將構造的時候說明的 addAll 方法,LinkedList 還提供了 add(int index, E element); 方法,下面我們來看在這個方法:

/**
 * 在指定 index 位置插入節點
 */
public void add(int index, E element) {
   // 檢查角標是否越界
   checkPositionIndex(index);
   // 如果 index = size 代表是在尾部插入節點
   if (index == size)
       linkLast(element);
   else
       linkBefore(element, node(index));
}
複製程式碼

可以先看到當 0 =< index <size 的時候呼叫了 linkBefore(element, node(index))方法,我們先來看下 node(index) 方法的實現:


/**
 * 返回一個非空節點,這個非空節點位於 index 位置
 */
 Node<E> node(int index) {
   // assert isElementIndex(index);
    // 如果 index < size/2 則從0開始尋找指定角標的節點
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
         // 如果 index >= size/2 則從 size-1 開始尋找指定角標的節點
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
 }
複製程式碼

大家可能會疑惑為什麼這裡註釋為返回一個非空節點?其實仔細想下就明白了,這裡的節點一定不為 null,如果一開始連結串列為空的時候,index 為 0 的位置肯定為 null,為什麼不會產生異常情況呢?其實如果一開始連結串列中沒有元素 size = 0,如果我們向 index = 0 的位置新增元素是不會走到 else 中的,而是會呼叫 linkLast(element); 方法去新增元素。 因此 node 方法可以用於根據指定 index 去以 size/2 為界限搜尋index 位置的 Node;

我們再看回 linkBefore 方法,為什麼要叫做 linkBefore 呢,因為在連結串列的中間位置新增節點,其實就是講 index 原來的節點前新增一個節點,新增節點我們需要知道該節點的前一個節點和當前節點,

  1. 將構造的新節點前指標 prev 指向 index 的前一個元素,
  2. 新節點前指標 next 指標指向 index 位置的節點,
  3. index 位置節點 prev 指標指向新節點
  4. index 位置前節點(pred)的 next 指標指向新節點。

linkBefore 也是做了上述四件事:

void linkBefore(E e, Node<E> succ) {
   // assert succ != null;
   // 由於 succ 一定不為空,所以可以直接獲取 prev 節點
   final Node<E> pred = succ.prev;
   // 新節點 prev 節點為 pred,next 節點為 succ
   final Node<E> newNode = new Node<>(pred, e, succ);
   // 原節點的 prev 指向新節點
   succ.prev = newNode;
   // 如果 pred 為空即頭節點出插入了一個節點,則將新的節點賦值給 first 索引
   if (pred == null)
       first = newNode;
   else
       pred.next = newNode;//否則 pred 的下一個節點改為新節點
   size++;
   modCount++;
}
複製程式碼

LinkedList 刪除節點的方法

與新增節點方法對應的就是刪除節點方法:

/**
 *  刪除頭節點
 * @return 刪除的節點的值 即 節點的 element
 * @throws NoSuchElementException 如果連結串列為空則丟擲異常
 */
 public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
 }

/**
 *  刪除尾節點
 *
 * @return  刪除的節點的值 即 節點的 element
 * @throws NoSuchElementException  如果連結串列為空則丟擲異常
 */
 public E removeLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
 }
 
複製程式碼

可以看出最終呼叫的方法為 unlinkFirstunlinkLast 方法:

/**
 * 移除頭節點
 */
 private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 頭節點的 element 這裡作為返回值使用
    final E element = f.item;
    // 頭節點下個節點
    final Node<E> next = f.next;
    // 釋放頭節點的 next 指標,和 element 下次 gc 的時候回收這個內部類
    f.item = null;
    f.next = null; // help GC
    // 將 first 索引指向新的節點
    first = next;
    // 如果 next 節點為空,即連結串列只有一個節點的時候,last 指向 null
    if (next == null)
        last = null;
    else
        next.prev = null; //否則 next 的 prev 指標指向 null
    size--;//改變連結串列長度
    modCount++;//修改運算元
    return element;//返回刪除節點的值
 }

/**
 * 移除未節點
 */
 private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    final E element = l.item;
    //未節點的前一個節點,
    final Node<E> prev = l.prev;
    //釋放未節點的內容
    l.item = null;
    l.prev = null; // help GC
    //將 last 索引指向新的未節點
    last = prev;
    // 連結串列只有一個節點的時候,first 指向 null
    if (prev == null)
       first = null;
    else
        prev.next = null;
    size--;
    modCount++;
    return element;
 }
複製程式碼

上邊我們說過在指定位置新增的節點時候的是4個步驟,移除頭尾節點是兩個特殊的節點,但是總體來說還是一樣的。下面看到 unlink(node(index))就是這樣的:

搞懂 Java LinkedList 原始碼

/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
   // assert x != null;
   final E element = x.item;
   //儲存 index 節點的前後兩個節點
   final Node<E> next = x.next;
   final Node<E> prev = x.prev;
    // 如果節點為頭節點,則做 unlinkFirst 相同操作
   if (prev == null) {
       first = next;
   } else {//否則將上一個節點的 next 指標指向下個節點
       prev.next = next;
       // 釋放 index 位置 prev 指標
       x.prev = null;
   }
    // 如果節點為尾節點,則將 last 索引指向上個節點
   if (next == null) {
       last = prev;
   } else {//否則下個節點 prev 指標指向上個節點
       next.prev = prev;
       x.next = null;
   }

   x.item = null;
   size--;
   modCount++;
   return element;
}
複製程式碼

看完 unlink 操作結合之前說的 node(index),下邊兩種刪除節點的操作,就很好理解了

/**
 * 刪除指定索引位置的節點
 */
public E remove(int index) {
   checkElementIndex(index);
   return unlink(node(index));
}

/**
 *刪除從頭節點其第一個與 o 相同的節點
 */
public boolean remove(Object o) {
    // 區別對待 null 元素,比較元素時候使用 == 而不是 equals
   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;
}
複製程式碼

看完單個刪除節點的方法 LinkedList 實現了 List 介面的 clear 操作,用於刪除連結串列所有的節點:

/**
* Removes all of the elements from this list.
* The list will be empty after this call returns.
*/
public void clear() {
   // 依次清除節點,幫助釋放記憶體空間
   for (Node<E> x = first; x != null; ) {
       Node<E> next = x.next;
       x.item = null;
       x.next = null;
       x.prev = null;
       x = next;
   }
   first = last = null;
   size = 0;
   modCount++;
}
複製程式碼

LinkedList 查詢節點的方法

LinkedList 查詢節點的方法,可分為根據指定的索引查詢,獲取頭節點,獲取未節點三種。值得注意的是,根據索引去獲取節點內容的效率並不高,所以如果查詢操作多餘增刪操作的時候建議用 ArrayList 去替代。

/**
* 根據索引查詢
*
public E get(int index) {
   checkElementIndex(index);
   return node(index).item;
}

/**
* 返回 first 索引指向的節點的內容
*
* @return the first element in this list
* @throws NoSuchElementException 如果連結串列為空則丟擲異常
*/
public E getFirst() {
   final Node<E> f = first;
   if (f == null)
       throw new NoSuchElementException();
   return f.item;
}

/**
* 返回 last 索引指向的節點的內容
*
* @return the last element in this list
* @throws NoSuchElementException 如果連結串列為空則丟擲異常
*/
public E getLast() {
   final Node<E> l = last;
   if (l == null)
       throw new NoSuchElementException();
   return l.item;
}

複製程式碼

LinkedList 的修改節點方法

修改節點也分為修改指定索引的節點內容和修改頭節點內容,未節點內容的方法? 哈哈,理所因當了,其實LinkedList 只提供了 set(int index, E element) 一個方法。

public E set(int index, E element) {
   // 判斷角標是否越界
   checkElementIndex(index);
   // 採用 node 方法查詢對應索引的節點
   Node<E> x = node(index);
   //儲存節點原有的內容值
   E oldVal = x.item;
   // 設定新值
   x.item = element;
   // 返回舊的值
   return oldVal;
}

複製程式碼

LinkedList 的元素查詢方法

上邊的我們知道 LinkedList提供根據角標查詢節點的方法,LinkedList 還提供了一系列判斷元素在連結串列中的位置的方法。

/* 
* 返回引數元素在連結串列的節點索引,如果有重複元素,那麼返回值為從**頭節點**起的第一相同的元素節點索引,
* 如果沒有值為該元素的節點,則返回 -1;
* 
* @param o element to search for
* @return 
*/
public int indexOf(Object o) {
   int index = 0;
  // 區別對待 null 元素,用 == 判斷,非空元素用 equels 方法判斷 
   if (o == null) {
       for (Node<E> x = first; x != null; x = x.next) {
           if (x.item == null)
               return index;
           index++;
       }
   } else {
       for (Node<E> x = first; x != null; x = x.next) {
           if (o.equals(x.item))
               return index;
           index++;
       }
   }
   return -1;
}

/**
**返回引數元素在連結串列的節點索引,如果有重複元素,那麼返回值為從**尾節點起**的第一相同的元素節點索引,
* 如果沒有值為該元素的節點,則返回 -1;
*
* @param o element to search for
* @return the index of the last occurrence of the specified element in
*         this list, or -1 if this list does not contain the element
*/
public int lastIndexOf(Object o) {
   int index = size;
   if (o == null) {
       for (Node<E> x = last; x != null; x = x.prev) {
           index--;
           if (x.item == null)
               return index;
       }
   } else {
       for (Node<E> x = last; x != null; x = x.prev) {
           index--;
           if (o.equals(x.item))
               return index;
       }
   }
   return -1;
}
複製程式碼

兩個方法分別返回從頭節點起第一個與引數元素相同的節點索引,和從尾節點起第一個與引數元素相同的節點索引。

除了上述兩個方法我們開可以呼叫 contains(Object o) 來判斷連結串列中是否有該元素存在。呼叫 indexOf 從頭結點開始查詢元素位置遍歷完成後若 返回值 !=-1 則表示存在,反之不存在

public boolean contains(Object o) {
    return indexOf(o) != -1;
}
複製程式碼

LinkedList 作為雙向佇列的增刪改查

分析完 LinkedList 作為 List 集合的增刪改查操作,我們看下 LinkedList 是如何實現 Deque 介面的方法的:

Deque 雙端佇列

我們先來認識一下 Java 中的 雙端佇列,我們都知道 Queue 是一個佇列,遵循 FIFO 準則,我們也知道 Stack 是一個棧結構,遵循 FILO 準則。 而Deque 這個雙端佇列就厲害了,它既可以實現棧的操作,也可以實現佇列的操作,換句話說,實現了這個介面的類,既可以作為棧使用也可以作為佇列使用。

我們來看下 Queue 給我們提供了的方法:

頭部 頭部 尾部 尾部
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
移除 removeFirst() pollFirst() remveLast() pollLast
獲取 getFirst() peekFirst() getLast() peekLast

由於 Deque 介面繼承 Queue 介面,當 Deque 當做佇列使用時(FIFO),只需要在頭部刪除,尾部新增即可。我們現在複習下 Queue 中的方法及區別:

  1. Queueofferadd 都是在佇列中插入一個元素,具體區別在於,對於一些 Queue 的實現的佇列是有大小限制的,因此如果想在一個滿的佇列中加入一個新項,多出的項就會被拒絕。此時呼叫 add()方法會丟擲異常,而 offer() 只是返回的 false。

  2. remove()poll() 方法都是從佇列中刪除第一個元素。remove()也將丟擲異常,而 poll() 則會返回 null

  3. element()peek() 用於在佇列的頭部查詢元素。在佇列為空時, element() 丟擲一個異常,而 peek() 返回 null

上述方法的區別對於 Queue 對應的實現類的對應方法,是一種規定,自己在實現 Queue 佇列的時候也要遵循此規則。

我們通過下邊的表格來對照下雙端佇列是如何實現佇列操作的,值得注意的是 Deque 實現了 Queue,所以 Queue 所有的方法 Deque 都有,下面比較的是Deque區別 Queue 的方法:

Queue Deque
add(e) addLast()
offer(e) offerLast()
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

由上表我們可以看到 Deque 對應的 Queue 的方法,那麼對於他們的實現類 LinkedList 是怎麼實現的呢?

Deque 和 Queue 新增元素的方法

我們對比下對應的實現方法:

// queue 的新增方法實現,
public boolean add(E e) {
   linkLast(e);
   return true;
}
// Deque 的新增方法實現,
public void addLast(E e) {
   linkLast(e);
} 
  
// queue 的新增方法實現,
public boolean offer(E e) {
   return add(e);
}

// Deque 的新增方法實現,
public boolean offerLast(E e) {
        addLast(e);
        return true;
}
    
複製程式碼

上面提及到 Queueofferadd 的區別針對容量有限制的實現,很明顯 LinkedList 的大小並沒有限制,所以在 LinkedList 中他們的實現並沒有實質性不同。

Deque 和 Queue 刪除元素的方法

// Queue 刪除元素的實現 removeFirst 會丟擲 NoSuchElement 異常
public E remove() {
   return removeFirst();
}

// Deque 的刪除方法實現
public E removeFirst() {
   final Node<E> f = first;
   if (f == null)
       throw new NoSuchElementException();
   return unlinkFirst(f);
}
    
// Queue 刪除元素的實現 不會丟擲異常 如果連結串列為空則返回 null 
public E poll() {
   final Node<E> f = first;
   return (f == null) ? null : unlinkFirst(f);
}

// Deque 刪除元素的實現 不會丟擲異常 如果連結串列為空則返回 null 
public E pollFirst() {
   final Node<E> f = first;
   return (f == null) ? null : unlinkFirst(f);
}
複製程式碼

Deque 和 Queue 獲取佇列頭部元素的實現

 // Queue 獲取佇列頭部的實現 佇列為空的時候回丟擲異常
 public E element() {
    return getFirst();
 }
// Deque 獲取佇列頭部的實現 佇列為空的時候回丟擲異常
public E getFirst() {
   final Node<E> f = first;
   if (f == null)
       throw new NoSuchElementException();
   return f.item;
}

// Queue 獲取佇列頭部的實現 佇列為空的時候返回 null
public E peek() {
   final Node<E> f = first;
   return (f == null) ? null : f.item;
}

// Deque 獲取佇列頭部的實現 佇列為空的時候返回 null
public E peekFirst() {
   final Node<E> f = first;
   return (f == null) ? null : f.item;
}
複製程式碼

上述我們分析了,雙端佇列作為佇列使用的時候的各個方法的區別,也可是看出 LinkedList 對對應方法的實現,遵循了佇列設計原則。

下面我們來看看下雙端佇列作為棧 Stack使用的時候方法對應關係,與 Queue 不同,Stack 本身就是實現類,他擁有 FILO 的原則, Stack 的入棧操作通過 push 方法進行,出棧操作通過 pop 方法進行,查詢操作通過 peek 操作進行。 Deque 作為棧使用的時候,也遵循 FILO 準則,入棧和出棧是通過新增和移除頭節點實現的。

Stack Deque
push(e) addFist(e)
pop() removeFirst()
peek() peekFirst()

由於分析佇列的時候已經分析了addFistremoveFirstpeekFirst操作的方法了,下邊我們來顯 push 和 pop 的實現:

public void push(E e) {
   addFirst(e);
}

public E pop() {
   return removeFirst();
}
複製程式碼

哇,毫無遮掩的直接呼叫了 addFirstremoveFirst 方法。這樣看來沒啥好分析的了。

LinkedList 的遍歷

ArrayList 分析的時候,我們就知道 List 的實現類,有4中遍歷方式:for 迴圈,高階 for 迴圈,Iterator 迭代器方法, ListIterator 迭代方法。由於 ArrayList 原始碼分析的時候比較詳細看了原始碼,對於不同資料結構的 LinkedList 我們只看下他們的不同之處.

LinkedList 沒有單獨 Iterator 實現類,它的 iteratorlistIterator 方法均返回 ListItr的一個物件。 LinkedList 作為雙向連結串列資料結構,過去上個元素和下個元素很方便。

下邊我們來看下 ListItr 的原始碼:

private class ListItr implements ListIterator<E> {
   // 上一個遍歷的節點
   private Node<E> lastReturned;
   // 下一次遍歷返回的節點
   private Node<E> next;
   // cursor 指標下一次遍歷返回的節點
   private int nextIndex;
   // 期望的運算元
   private int expectedModCount = modCount;
    
   // 根據引數 index 確定生成的迭代器 cursor 的位置
   ListItr(int index) {
       // assert isPositionIndex(index);
       // 如果 index == size 則 next 為 null 否則尋找 index 位置的節點
       next = (index == size) ? null : node(index);
       nextIndex = index;
   }

   // 判斷指標是否還可以移動
   public boolean hasNext() {
       return nextIndex < size;
   }
    
  // 返回下一個帶遍歷的元素
  public E next() {
       // 檢查運算元是否合法
       checkForComodification();
       // 如果 hasNext 返回 false 丟擲異常,所以我們在呼叫 next 前應先呼叫 hasNext 檢查
       if (!hasNext())
           throw new NoSuchElementException();
        // 移動 lastReturned 指標
       lastReturned = next;
        // 移動 next 指標
       next = next.next;
       // 移動 nextIndex cursor
       nextIndex++;
       // 返回移動後 lastReturned
       return lastReturned.item;
   }

  // 當前遊標位置是否還有前一個元素
   public boolean hasPrevious() {
       return nextIndex > 0;
   }
  
  // 當前遊標位置的前一個元素
   public E previous() {
       checkForComodification();
       if (!hasPrevious())
           throw new NoSuchElementException();
        // 等同於 lastReturned = next;next = (next == null) ? last : next.prev;
        // 發生在 index = size 時
       lastReturned = next = (next == null) ? last : next.prev;
       nextIndex--;
       return lastReturned.item;
   }
    
   public int nextIndex() {
       return nextIndex;
   }

   public int previousIndex() {
       return nextIndex - 1;
   }
    
    // 刪除連結串列當前節點也就是呼叫 next/previous 返回的這節點,也就 lastReturned
   public void remove() {
       checkForComodification();
       if (lastReturned == null)
           throw new IllegalStateException();

       Node<E> lastNext = lastReturned.next;
       //呼叫LinkedList 的刪除節點的方法
       unlink(lastReturned);
       if (next == lastReturned)
           next = lastNext;
       else
           nextIndex--;
       //上一次所操作的 節點置位空    
       lastReturned = null;
       expectedModCount++;
   }

    // 設定當前遍歷的節點的值
   public void set(E e) {
       if (lastReturned == null)
           throw new IllegalStateException();
       checkForComodification();
       lastReturned.item = e;
   }
    // 在 next 節點位置插入及節點
   public void add(E e) {
       checkForComodification();
       lastReturned = null;
       if (next == null)
           linkLast(e);
       else
           linkBefore(e, next);
       nextIndex++;
       expectedModCount++;
   }
    //簡單哈運算元是否合法
   final void checkForComodification() {
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
   }
}
複製程式碼

參考下圖理解下,LinkedList 的迭代器的三個變數。

搞懂 Java LinkedList 原始碼

搞懂 Java LinkedList 原始碼

搞懂 Java LinkedList 原始碼

總結

本文從 LinkedList 的原始碼出發,分析了LinkedList 集合常見的操作,以及它作為佇列或者棧的時候增刪改查方法。是繼上一篇 ArrayList原始碼分析後的第二篇集合框架原始碼分析。

你見過凌晨3點北京的太陽嗎?沒有!三點太陽還沒升起呢~

相關文章