Java容器類框架分析(2)LinkedList原始碼分析

wustor發表於2019-01-21

概述

在分析LinkedList的原始碼之前,先看一下ArrayList在資料結構中的位置,常見的資料結構按照邏輯結構跟儲存結構可以做如下劃分:

資料結構分類
資料結構分類

先看看原始碼的註釋:

  • Doubly-linked list implementation of the {@code List} and {@code Deque}
    interfaces. Implements all optional list operations, and permits all
    elements (including {@code null}).
    All of the operations perform as could be expected for a doubly-linked
    list. Operations that index into the list will traverse the list from
    the beginning or the end, whichever is closer to the specified index.
  • LinkedList是一個實現了List介面跟Deque的雙連結串列。實現了所有的List介面的操作,允許存放任意元素(包括空值).能夠進行雙連結串列的所有操作插入操作,LinkedList中的索引操作將會從頭到尾遍歷整個連結串列,知道找到具體的索引。

從註釋中可以看出,LinkedList是一個雙向非迴圈連結串列,並且實現了Deque介面,還是一個雙端佇列,所以比ArrayList要複雜一些。

正文

連結串列

在分析LinkedList之前我們先複習一下連結串列這種資料結構

連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的。連結串列由一系列結點(連結串列中每一個元素稱為結點)組成,結點可以在執行時動態生成。每個結點包括兩個部分:一個是儲存資料元素的資料域,另一個是儲存下一個結點地址的指標域。

連結串列按照指向可以分為單向連結串列跟雙向連結串列,也可以按照是否迴圈氛分為迴圈連結串列跟非迴圈連結串列。

連結串列分類
連結串列分類

單向連結串列

單向連結串列
單向連結串列

從head節點開始,next指標只想下一個節點的資料,tail節點的next指標指向null

雙向連結串列

雙向連結串列
雙向連結串列

每個節點有連個指標,pre跟next,除了head節點pre指標跟tail節點的next指標都指向null之外,其餘的相鄰節點的指標不管是從頭到尾還是反過來,當前節點的兩個指標包含了相鄰節點的指向。

單向迴圈連結串列

單向迴圈連結串列
單向迴圈連結串列

單向迴圈連結串列跟單向連結串列的區別在於,tail節點指向head節點的資料

雙向迴圈連結串列

雙向迴圈連結串列
雙向迴圈連結串列

雙向迴圈連結串列跟單向迴圈連結串列可以進行類比,只是把head節點的pre指標跟tail節點的next指標分別指向tail跟head的資料區域而已。

ArrayList原始碼分析

先看一下ArrayList的繼承關係

LinkedList的繼承關係
LinkedList的繼承關係
  • 虛線代表實現關係
  • 實線代表繼承,其中藍色的代表類之間繼承關係,綠色代表介面之間的繼承關係

跟ArrayList的區別在於LinkedList實現了Deque這個介面,Deque則繼承自Queue這個介面,所以LinkedList能夠進行佇列操作,其餘的實現跟ArrayList基本一樣,不再多說,下面開始分析LinkedList的原始碼。

成員變數

//序列化
private static final long serialVersionUID = 876323262645176354L;
transient int size = 0;//元素個數
transient Node<E> first;//head結點
transient Node<E> last;//tail節點

//內部類節點
private static class Node<E> {
    E item;儲存的資料
    Node<E> next;//next指標,指向下一個資料
    Node<E> prev;//pre指標,指向上一個資料

    Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }複製程式碼

建構函式

  • 空的建構函式(Constructs an empty list.)
    public LinkedList() {
    }複製程式碼

當我們通過此構造方法進行初始化LinkedList的時候,實際上什麼都沒做,此時只有一個Node,data為null,pre指向null,next也指向null。

  • 通過Collection來構造(Constructs a list containing the elements of the specified collection, in the order they are returned by the collection`s iterator.)
//呼叫addAll
  public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
//緊接著呼叫addAll(size, c)
      public boolean addAll(Collection<? extends E> c) {
      return addAll(size, c);
    }

  //這個方法比較關鍵,因為不管是初始化,還是進行新增,都會呼叫此方法,下面重點分析一下
    public boolean addAll(int index, Collection<? extends E> c) {
         //檢查index是否合法
        checkPositionIndex(index);
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew == 0)
            return false;
        //初始化兩個Node,保留下一個節點,當集合新增完成之後,需要跟此節點進行連線,構成連結串列
        Node<E> pred, succ;
          //插入的時候就是分兩種,一種是從尾部插入,一種是從中間插入
        if (index == size) {
        //在尾部插入
            succ = null;//null值作為後面連線的一個標誌
            pred = last;//將pred指向上一個節點也就是tail節點
        } 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
            //Node個數>0,當前指標指向新的節點
                pred.next = newNode;
            //移到下一個節點
            pred = newNode;
        }
     //連結串列新增完畢,開始從斷開的地方進行連線
        if (succ == null) {
        //尾部插入進行連線,此時last需要重新賦值,即為pred節點
            last = pred;
        } else {
        //中間插入,直接講集合的最後一個節點跟之前插入點後的節點進行連線就好
            pred.next = succ;將當前Node的next指標指向下一個節點
            succ.prev = pred;//將下一個節點的pre指向pre
        }

        size += numNew;
        modCount++;
        return true;
    }複製程式碼

結合圖形來理解一下

雙向連結串列插入元素
雙向連結串列插入元素

稍微總結一下,這個addAll實際上就是先把連結串列打斷,然後從斷的左側進行新增一些元素,新增完成之後再將連結串列進行連線起來,恩,就是這個樣子,歸納一下就是:

  • 將連結串列打斷,用兩個節點保留插入位置的下一個節點
  • 在節點插入完成之後,再進行連線
  • 需要注意插入的位置:是在尾部還是中間插入,因為兩者最後進行連結串列重連的方式不一樣。

add方法

LinkedList的Add方法
LinkedList的Add方法

通過檢視實際上有很多,這裡就不一一貼出來了,最終呼叫的都是這幾個方法:

在頭部插入(Links e as first element.)
   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
        //將原先的頭結點的pre指標指向新的頭結點
            f.prev = newNode;
        size++;
        modCount++;
    }複製程式碼
在尾部插入(Links e as last element.)
   void linkLast(E e) {
         //拿到尾節點
        final Node<E> l = last;
          //初始化一個Node,也就是新的尾節點
        final Node<E> newNode = new Node<>(l, e, null);
        //將新的尾節點賦值給last
        last = newNode;
        //尾結點為空
        if (l == null)
        //此時只有一個節點,所以當前節點即是頭結點也是尾節點
            first = newNode;
        else
        //將原先的尾節點指向現在的新的尾節點
            l.next = newNode;
        size++;
        modCount++;
    }複製程式碼
在某個元素之前插入(Inserts element e before non-null 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++;
    }複製程式碼

remove操作

有如下幾個方法

Remove操作
Remove操作

跟add操作相對應,也只是改變相應的連結串列的指向而已,我們選擇一個來看看:

   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 {
        //將刪除節點的前一個節點的next指向後一個節點
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
        //尾結點,刪除之後,尾節點前移
            last = prev;
        } else {
        //將刪除節點的後一個節點的pre指向前一個節點
            next.prev = prev;
            x.next = null;
        }

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

說到底還是在改變Node節點的指向而已

set操作

    public E set(int index, E element) {
        checkElementIndex(index);//檢查索引
        Node<E> x = node(index);//拿到需要修改的那個節點
        E oldVal = x.item;//拿到修改的節點的值
        x.item = element;//進行修改
        return oldVal;
    }複製程式碼

查詢操作

    public E getFirst() {
        final Node<E> f = first;//拿到head節點
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

    public E getLast() {
        final Node<E> l = last;////拿到tail節點
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }
    //獲取某一個索引的節點

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

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

contains操作

        public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

        public int indexOf(Object o) {
        int index = 0;
        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;
    }複製程式碼

沒有什麼好說的,就是遍歷查詢而已,這裡會發現,LinkedList的查詢很低效,需要遍歷整個集合。

佇列操作

push

    public void push(E e) {
        addFirst(e);
    }複製程式碼

offer

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

peek

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

pop

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

poll

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

getFirst

  public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }複製程式碼

getLast

   public E getLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return l.item;
    }複製程式碼

上面都是關於佇列的一些操作,用連結串列也可以實現,而且操作比較簡單,可以看做是佇列的一種連結串列實現方式。

總結

  • 底層是通過雙向連結串列來實現的,但是並非迴圈連結串列。
  • 不需要擴容,因為底層是線性儲存
  • 增刪快,但是查詢比較慢
  • 非執行緒安全

相關文章