1. 程式人生 > >LinkedList原始碼分析(jdk1.8)

LinkedList原始碼分析(jdk1.8)

一、概述

在這裡插入圖片描述
上圖為LinkedList的繼承結構圖,根據繼承關係總結如下:

LinkedList 是一個繼承於AbstractSequentialList的雙向連結串列。它也可以被當作堆疊、佇列或雙端佇列進行操作。
LinkedList 實現 List 介面,能對它進行佇列操作。
LinkedList 實現 Deque 介面,即能將LinkedList當作雙端佇列使用。
LinkedList 實現了Cloneable介面,即覆蓋了函式clone(),能克隆。
LinkedList 實現java.io.Serializable介面,這意味著LinkedList支援序列化,能通過序列化去傳輸。
LinkedList 是非同步的。

二、原始碼分析

1、成員變數

   //集合元素數量
    transient int size = 0;

   //連結串列頭結點
    transient Node<E> first;

    //連結串列尾結點
    transient Node<E> last;

LinkedList的屬性不多,size表示其元素數量,first表示連結串列頭節點,而last表示連結串列尾節點。其中連結串列是由節點(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)為LinkedList的靜態內部類,且有前置節點和後置節點 ,只是頭節點的前置節點為空,而尾節點的後置節點為空,說明LinkedList內部為雙向連結串列結構。可以用如下的圖形象的表示:
在這裡插入圖片描述

2、構造方法

   //預設構造方法
    public LinkedList() {
    }

   //通過集合c的元素構造連結串列
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);//將集合c中的所有元素新增到LinkedList中
    }

不傳參的構造方法建立空的連結串列,將集合作為入參的構造方法則通過集合c的元素建立連結串列,呼叫了addAll方法,addAll方法的原理將在下面講解。

3、常用方法

我們從LinkedList集合的常用操作入手,比如集合的增刪改查,棧的進棧和出棧,佇列的進佇列和出佇列等。

3.1、增

方法名 功能
linkFirst(E e) 新增元素e到連結串列的頭部
linkLast(E e) 新增元素e到連結串列的尾部
addFirst(E e) 新增元素e到連結串列的頭部,呼叫 linkFirst方法,無返回值
addLast(E e) 新增元素e到連結串列的尾部,呼叫linkLast方法,無返回值
add(E e) 新增元素e到連結串列的尾部,呼叫linkLast方法
add(int index, E element) 新增元素e到連結串列的指定位置index處
offer(E e) 新增元素e到連結串列的尾部,呼叫 add(E e) 方法
offerFirst(E e) 新增元素e到連結串列的頭部,呼叫 addFirst(E e)方法,返回true或false
offerLast(E e) 新增元素e到連結串列的尾部,呼叫 addLast(E e)方法,返回true或false
addAll(Collection<? extends E> c) 新增集合c的所有元素到連結串列的尾部
addAll(int index, Collection<? extends E> c) 新增集合c的所有元素到連結串列的指定位置 處
linkBefore(E e, Node succ) 將元素e插入到連結串列的指定節點succ前
push(E e)(E e, Node succ) 將元素e插入到棧上,無返回值,其實就是呼叫了addFirst(E e)方法

有上面的表格可知,只要理解了linkFirst,linkLast方法的原始碼,大部分方法的原理都搞懂了,首先來看下linkFirst,linkLast的原始碼:

   //連結串列的頭部插入元素e
    private void linkFirst(E e) {
        //獲取連結串列的頭節點
        final Node<E> f = first;
        //通過元素e建立新節點,其前置節點為空,後置結點為當前連結串列的頭節點
        final Node<E> newNode = new Node<>(null, e, f);
        //將連結串列的頭結點設定為新節點
        first = newNode;
        //若原本的頭節點為空,說明為空連結串列,將尾結點也設定為新節點;若原本的頭節點不為空,就將其前置結點設定為新節點;
        //此時完成了頭部插入節點的操作
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
         //將元素大小和連結串列修改記錄分別+1
        size++;
        modCount++;
    }
  
  //連結串列的尾部插入元素e
    void linkLast(E e) {
        //獲取連結串列的頭結點
        final Node<E> l = last;
         //通過元素e建立新節點,其前置結點當前列表的尾節點,後置節點為空
        final Node<E> newNode = new Node<>(l, e, null);
        //將連結串列的尾結點設定為新節點
        last = newNode;
         //若原本的尾節點為空,說明為空連結串列,將頭結點也設定為新節點;若原本的尾節點不為空,就將其後置結點設定為新節點;
        //此時完成了尾部插入結點的操作
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
         //將元素大小和連結串列修改記錄分別+1
        size++;
        modCount++;
      }

    public void addFirst(E e) {
        linkFirst(e);
    }
    public void addLast(E e) {
    	linkLast(e);
	   }
       public E offer(E e) {
   	      return add(e);
      }
      public boolean offerFirst(E e) {
   		 addFirst(e);
   		 return true;
     }
    public boolean offerLast(E e) {
    	addLast(e);
   		 return true;
    }
  public void push(E e) {
   	      addFirst(e);
 }

add(int index, E element) :在指定位置新增元素
在add(int index, E element)方法中呼叫了node(int index) 來獲取指定下標的節點,很多地方也呼叫了該方法。它採取了二分法的方式去獲取值,可以提高執行效率。

        //返回指定位置的節點
	 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;
	        }
	    }

瞭解了node(int index)後再進一步看add(int index, E element) 方法的執行過程

    public void add(int index, E element) {
        //檢測下標的邊界是否合法
        checkPositionIndex(index);

        if (index == size)
          //若index == size說明往尾部插入元素,直接呼叫linkLast方法;否則呼叫linkBefore方法
            linkLast(element);
        else
            linkBefore(element, node(index));
    }	       
       //將元素e插入到連結串列的指定節點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++;
	    }

add(int index, E element)的執行過程可以用下圖表示,可以知道LinkedList的隨機插入效率比ArrayList高,因為ArrayList需要移動陣列中相應的元素而它不需要。
在這裡插入圖片描述

addAll方法
addAll(Collection<? extends E> c):在尾部插入集合c中的元素
addAll(int index, Collection<? extends E> c):在指定位置index插入集合c中的元素

//在尾部插入集合c中的元素
 public boolean addAll(Collection<? extends E> c) {
       //呼叫addAll(int index, Collection<? extends E> c)方法,當index=size時就是從尾部位置插入
        return addAll(size, c);
   }
 
 //在指定位置index插入集合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;
	
	//index節點的前置節點,後置節點
        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);
            if (pred == null)
                first = newNode;
            else
                pred.next = newNode;
            //步進,當前的節點為前置節點了,為下次新增節點做準備
            pred = newNode;
        }
	
        if (succ == null) {//迴圈結束後,判斷,如果後置節點是null。 說明此時是在隊尾append的。
            last = pred;//將尾節點設定為後置節點
        } else {
            pred.next = succ;//否則是在隊中插入的節點 ,更新前置節點的後置節點
            succ.prev = pred;//再更新後置節點的前置節點
        }

        size += numNew;
        modCount++;
        return true;
    }		

addAll方法主要分為4步:

  1. 檢查index索引的邊界值
  2. 將集合資料 轉換為陣列
  3. 得到插入位置index的前置節點和後置節點
  4. 遍歷資料,將資料插入到指定位置

3.2、刪

方法名 功能
unlink(Node x) 刪除節點
removeFirst 刪除首節點並返回該刪除首節點,呼叫方法 unlinkFirst(Node f)
removeLast 刪除尾節點並返回該刪除尾節點,呼叫方法 unlinkLast(Node l)
remove(int index) 刪除指定位置節點並返回該刪除節點
remove(Object o) 刪除首次出現的指定的節點,刪除成功返回true,不存在返回false
removeFirstOccurrence(Object o) 刪除首次出現的指定的節點,刪除成功返回true,不存在返回false,呼叫方法remove(Object o)
removeLastOccurrence(Object o) 刪除最後一次出現的指定的節點,刪除成功返回true,不存在返回false
poll 檢索並刪除此連結串列的頭節點,不存在返回空
pollFirst 檢索並刪除此連結串列的頭節點,不存在返回空
pollLast 檢索並刪除此連結串列的尾節點,不存在返回空

unlink(Node x):刪除指定節點x

 //取消連結串列的非空節點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;
    }

removeFirst:刪除首節點並返回該刪除首節點,呼叫方法 unlinkFirst(Node f)

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
   
 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; //首節點置為空,剩下的交給GC處理
        first = next;//將連結串列的首節點設為後置節點
        if (next == null)
            last = null;//若後置節點為空,說明為尾節點,此時將尾節點設為空
        else
            next.prev = null;//若後置節點不為空,將其前置節點設為空,此時相當於變成了首節點
        size--;
        modCount++;
        return element;
    }	 

removeLast :刪除尾節點並返回該刪除尾節點,呼叫方法 unlinkLast(Node l)

    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
     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; //尾節點置為空,剩下的交給GC處理
        last = prev;//將連結串列的尾節點設為前置節點
        if (prev == null)
            first = null;//若前置節點為空,說明為首節點,此時將首節點設為空
        else
            prev.next = null;//若前置節點不為空,將其後置節點設為空,此時相當於變成了尾節點
        size--;
        modCount++;
        return element;
    }

remove(int index): 刪除指定位置節點並返回該刪除節點

public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}

刪除流程主要為:
1.得到待刪除節點的前置節點和後置節點
2.刪除前置節點
3.刪除後置節點
在這裡插入圖片描述

remove(Object o):刪除首次出現的指定的節點,刪除成功返回true,不存在返回false。
該方法就是遍歷元素然後匹配的話就呼叫刪除節點方法unlink(Node x)。

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

3.3、改

set(int index, E element):設定指定位置的元素並返回原來的元素

	public E set(int index, E element) {
	//檢查索引邊界值
        checkElementIndex(index);
        //獲取當前位置的節點
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

3.4、查

方法名 功能
node(int index) 獲取指定位置處的節點
get(int index) 獲取指定位置處的元素
getFirst 獲取連結串列的第一個元素
getLast 獲取連結串列的最後一個元素
element 獲取連結串列的第一個元素,呼叫方法getFirst
peek 獲取連結串列的第一個元素 ,不存在則返回空
peekFirst 獲取連結串列的第一個元素 ,不存在則返回空
peekLast 獲取連結串列的最後一個元素 ,不存在則返回空

node(int index)方法在前面已經提過,它通過二分法的方式縮小遍歷範圍,一定程度上提高了檢索效率,不過肯定比不上ArrayList的檢索效率。

get(int index):獲取指定位置處的節點

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

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

三、總結

  • LinkedList不是執行緒安全的。
  • LinkedList底層基於雙向連結串列,隨機插入和刪除效率比ArrayList高,但跟據索引查詢的效率比ArrayList低。
  • LinkedList相比ArrayList沒有擴容操作,ArrayList可能會浪費一定的容量空間,但LinkedList每個節點都要儲存前後節點的引用,需要消耗相當的空間,就儲存密度來說,ArrayList優於LinkedList。
  • 可以使用LinkedList作為佇列和棧的實現。

參考連結:
https://www.imooc.com/article/78970
https://blog.csdn.net/zxt0601/article/details/77341098
https://blog.csdn.net/qq_19431333/article/details/54572876
https://www.cnblogs.com/xujian2014/p/4630785.html#_label2