LinkedList詳解-原始碼分析

微風細語0805發表於2020-05-21

LinkedList詳解-原始碼分析

LinkedList是List介面的第二個具體的實現類,第一個是ArrayList,前面一篇文章已經總結過了,下面我們來結合原始碼,學習LinkedList。

  • 基於雙向連結串列實現

  • 便於插入和刪除,不便於遍歷

  • 非執行緒安全

  • 有序(連結串列維護順序)

  • ...

上面是LinkedList的一些特性。

1. LinkedList類宣告

原始碼如下所示:

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

初步分析:

  • 繼承了AbstractSequentialList抽象類
  • 實現了List、Deque、Cloneable、Serializable介面

思考:

  • List、Cloneable、Serializable介面在上一篇ArrayList詳解裡已經分析過了,這個Deque介面是幹嘛的呢?

咳咳,先谷歌一下,發現Deque的意思是雙端佇列,這裡已經可以看出LinkedList是基於雙向連結串列的一些端倪了,帶著這點疑問,我們繼續往下看。

2. 成員變數

原始碼如下所示:

    transient int size = 0;

    transient Node<E> first;

    transient Node<E> last;

private static final long serialVersionUID = 876323262645176354L;

比ArrayList的成員變數少了好幾個呢。

初步分析:

  • size依然是集合內的元素個數
  • transient關鍵字標識變數不會被序列化
  • Node是節點的意思,具體程式碼是什麼樣的?

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;
        }
    }

注意:此處只擷取了Node的程式碼,Node是LinkedList的靜態內部類,還是在LinkedList.class檔案內部的

分析:

  • 宣告瞭三個成員變數
  • 分別表示當前元素,下一個節點,上一個節點

也就是說,LinkedList的每一個元素都是一個Node,而每一個Node都儲存了三部分內容,由此也就證實了LinkedList是基於雙向連結串列的。

3. 構造方法

原始碼如下所示:

 	public LinkedList() {
    }
  
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

分析:

  • LinkedList提供了兩個構造方法
  • 分別是對應無參構造和傳入Collection子類進行構造

可以發現,相對於ArrayList,LinkedList類並沒有指定容量的構造,這是為什麼呢?

思考:

1. 這就是ArrayList和LinkedList底層依賴不同有關係,ArrayList底層是陣列,LinkedList底層是雙向連結串列。陣列初始化是需要宣告長度的,連結串列則不需要。

2. 傳入子類進行構造時,也是呼叫了無參構造方法,再呼叫addAll()方法,將所有元素新增進去

4. 常用方法分析

addFirst(E e)

原始碼如下所示:

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++;
    }

addFirst()方法是在連結串列頭部插入一個元素,分析如下:

  • 先獲取到原來的頭節點,賦值給f
  • 建立一個新的節點newNode,該節點的next節點為f
  • newNode賦值給first
  • 如果原來的頭節點是null的話,說明此時連結串列是空的,新增的是第一個元素,則將newNode也賦值給last節點
  • 如果原來的頭節點不是null,那麼將原來的頭節點fpreNode設定為newNode
  • 連結串列長度加1,連結串列修改次數加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++;
    }

add()方法預設是在連結串列的尾部進行新增元素。

分析:

  • 此處的linkLast方法是不是很眼熟?和前面的linkeFirst基本一致噢
  • 不同之處僅在於,linkFirst是對對頭節點進行變更,而linkLast是對尾節點進行變更
  • 此處不再贅述

get(int index)

原始碼如下所示:

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

get()方法內隱藏著LinkedList不便於進行遍歷的真相!一定要搞明白哦。

分析:

  • 第一步先確認index是否在正確的範圍內,範圍為(0~size)
  • 第二步呼叫node方法返回對應索引位置的節點元素

node()方法原始碼如下:

    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;
        }
    }

分析如下:

  • 首先比較index和連結串列長度的1/2的大小
  • 如果index小於連結串列長度的1/2,那麼就會從頭節點向index位置進行遍歷,直到獲取到相應節點並返回該節點
  • 如果index大於連結串列長度的1/2,那麼從尾節點向index位置進行遍歷,直到獲取到相應節點並返回該節點

可以看出,當你訪問的元素越靠近連結串列的中間,那麼獲取該元素所花費的時間就會越長,所以LinkedList在遍歷上是比較慢的,連結串列本身是不支援任意性訪問的,雖然LinkedList的get()方法可以讀到相應元素,但是效率很低,不建議使用。

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;
    }

分析如下:

  • 元素為null時,使用==進行元素內容的判斷,然後呼叫unlink方法
  • 元素不為null時,使用equals方法進行判斷兩個元素是否相同,然後呼叫unlink方法

unlink方法原始碼如下所示:

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;
    }

哇,unlink方法原始碼有點長啊,容我慢慢道來:

  • 定義三個變數分別接收傳入節點x的內容、上一個節點、下一個節點
  • 如果節點x的上一個節點為null的話,說明x節點是頭節點,那麼就將x節點的下一個節點賦值給頭節點
  • 如果節點x不是頭節點,則將x節點的下一個節點賦值給上一個節點的next節點,並將x節點的上一個節點置為null
  • 經上面兩步,已經完成了x節點和上一個節點的斷開,以及下一個節點和x節點的上一個節點的連結
  • 如果x節點的下一個節點為null,說明x節點是尾節點,那麼就將x節點的上一個節點賦值給last節點
  • 如果x節點不是尾節點,那麼將x節點的上一個節點,賦值給下一個節點的prev節點,並將x節點的下一個節點置為null
  • 經過上面幾步之後,x節點就已經從連結串列中移除了
  • 然後將x節點的節點內容置為null,連結串列長度減1,修改長度記錄加1
  • 返回刪除節點的內容element

removeFirst()

原始碼如下所示:

    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }

分析:

  • 獲取連結串列頭節點,賦值給f
  • 如果f等於null,說明此時連結串列是空的,丟擲異常
  • 如果f不等於null,呼叫unlinkFirst方法,傳入f

unlinkFirst()方法原始碼如下所示:

    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;
    }

分析:

  • 獲取f節點內容,賦值給element變數,獲取fnext節點賦值給變數next
  • f節點內容,f節點的next節點,均賦值為null,等待GC回收
  • next節點賦值給first
  • 如果nextnull的話,說明此時連結串列為空了,所以將尾節點last也賦值為null
  • 否則,將next節點的prev成員變數賦值為null
  • 連結串列長度減1,修改記錄數加1
  • 返回被移除的元素

5. 其他方法概述

LinkedList可以作為FIFO(First In First Out)的佇列,也就是先進先出的佇列使用,以下是關於佇列的操作。

    //獲取佇列的第一個元素,如果為null會返回null
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }
	//獲取佇列的第一個元素,如果為null會丟擲異常
    public E element() {
        return getFirst();
    }
	//獲取佇列的第一個元素,如果為null會返回null
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
	//獲取佇列的第一個元素,如果為null會丟擲異常.
    public E remove() {
        return removeFirst();
    }
	//將元素新增到佇列尾部
    public boolean offer(E e) {
        return add(e);
    }

LinkedList也可以作為棧使用,棧的特性是LIFO(Last In First Out),也就是後進先出。 新增和刪除元素都只操作佇列的首節點即可。

原始碼如下:

	public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }
	
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }
 	
    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }
	
    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }
	
    public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
	
    public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }
	
    public void push(E e) {
        addFirst(e);
    }
	
    public E pop() {
        return removeFirst();
    }
	
    public boolean removeFirstOccurrence(Object o) {
        return remove(o);
    }

	
	public boolean removeLastOccurrence(Object o) {
     	
        if (o == null) {
			for (Node<E> x = last; x != null; x = x.prev) {
                if (x.item == null) {
                    //呼叫unlink方法刪除指定節點
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = last; x != null; x = x.prev) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

6. 總結

LinkedList相對於ArrayList而言,原始碼並沒有很複雜,從原始碼中我們得知了以下相關資訊:

  • LinkedList是基於雙向連結串列實現的,即每一個節點都儲存了上一個節點和下一個節點的資訊
  • LinkedList根據索引獲取元素效率低的原因是因為它需要一個節點一個節點的遍歷,獲取首節點和尾節點很快
  • LinkedList實現了Deque介面,具有雙向佇列的性質,可以實現資料結構中的堆疊。
  • ...

知之為知之,不知為不知,是知也。

相關文章