jdk1.8 LinkedList源碼全分析

本文原創地址,我的博客https://jsbintask.cn/2019/03/26/jdk/jdk8-linkedlist/(食用效果最佳),轉載請註明出處!

前言

LinkedList內部是一個鏈表的實現,一個節點除了保持自身的數據外,還持有前,後兩個節點的引用。所以就數據存儲上來說,它相比使用數組作爲底層數據結構的ArrayList來說,會更加耗費空間。但也正因爲這個特性,它刪除,插入節點很快!LinkedList沒有任何同步手段,所以多線程環境須慎重考慮,可以使用Collections.synchronizedList(new LinkedList(...));保證線程安全。

友情鏈接:jdk1.8 ArrayList源碼全分析

LinkedList結構

類關係


這裏我們需要注意的是,相比於ArrayList,它額外實現了雙端隊列接口Deque,這個接口主要是聲明瞭隊頭,隊尾的一系列方法。

類成員


LinkedList內部有兩個引用,一個first,一個last,分別用於指向鏈表的頭和尾,另外有一個size,用於標識這個鏈表的長度,而它的接的引用類型是Node,這是他的一個內部類:

很容易理解,item用於保存數據,而prve用於指向當前節點的前一個節點,next用於指向當前節點的下一個節點。

源碼解析

add(E e)方法

public boolean add(E e) {
    linkLast(e);
    return true;
}

這個方法直接調用linkLast:

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

我們用作圖來解釋下這個方法的執行過程,一開始,first和last都爲null,此時鏈表什麼都沒有,當第一次調用該方法後,first和last均指向了第一個新加的節點E1:



接着,第二次調用該方法,加入新節點E2。首先,將last引用賦值給l,接着new了一個新節點E2,並且E2的prve指向l,接着將新節點E2賦值爲last。現在結構如下:



接着判斷l==null? 所以走的else語句,將l的next引用指向新節點E2,現在數據結構如下:

接着size+1,modCount+1,退出該方法,局部變量l銷燬,所以現在數據結構如下:



這樣就完成了鏈表新節點的構建。

add(int index, E element) 這個方法是在指定位置插入新元素

public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
  1. index位置檢查(不能小於0,大於size)
  2. 如果index==size,直接在鏈表最後插入,相當於調用add(E e)方法
  3. 小於size,首先調用node方法將index位置的節點找出,接着調用linkBefore
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++;
}

我們同樣作圖分析,假設現在鏈表中有三個節點,調用node方法後找到的第二個節點E2,則進入方法後,結構如下:



接着,將succ的prev賦值給pred,並且構造新節點E4,E4的prev和next分別爲pred和suc,同時將新節點E4賦值爲succ的prev引用,則現在結構如下:



接着,將新節點賦值給pred節點的next引用,結構如下:

最後,size+1,modCount+1,推出方法,本地變量succ,pred銷燬,最後結構如下:



這樣新節點E4就插入在了第二個E2節點前面。新鏈表構建完成。從這個過程中我們可以知道,這裏並沒有大量移動移動以前的元素,所以效率非常高!

E get(int index)獲取指定節點數據

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

直接調用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;
    }
}
  1. 判斷index在鏈表的哪邊。
  2. 遍歷查找index或者size-index次,找出對應節點。
    這裏我們知道,相比於數組的直接索引獲取,遍歷獲取節點效率並不高。

E remove(int index)移除指定節點

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
  1. 檢查index位置
  2. 調用node方法獲取節點,接着調用unlink(E e)
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;
}

這個方法就不做分析了,其原理就是將當前節點X的前一個節點P的next直接指向X的下一個節點D,這樣X就不再關聯任何引用,等待垃圾回收即可。
這裏我們同樣知道,相比於ArrayList的copy數組覆蓋原來節點,效率同樣更高!

到現在,我們關於鏈表的核心方法,增刪改都分析完畢,最後介紹下它實現的隊列Deque的各個方法:

  • add(E e):隊尾插入新節點,如果隊列空間不足,拋出異常;LinkedList沒有空間限制,所以可以無限添加。
  • offer(E e):隊尾插入新節點,空間不足,返回false,在LinkedList中和add方法同樣效果。
  • remove():移除隊頭節點,如果隊列爲空(沒有節點,first爲null),拋出異常。LinkedList中就是first節點(鏈表頭)
  • poll():同remove,不同點:隊列爲空,返回null
  • element():查詢隊頭節點(不移除),如果隊列爲空,拋出異常。
  • peek():同element,不同點:隊列爲空,返回null。

總結

  1. LinkedList內部使用鏈表實現,相比於ArrayList更加耗費空間。
  2. LinkedList插入,刪除節點不用大量copy原來元素,效率更高。
  3. LinkedList查找元素使用遍歷,效率一般。
  4. LinkedList同時是雙向隊列的實現。

關注我,這裏只有乾貨!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章