jdk1.8-Java集合框架之--LinkedList源碼分析

前戲作足

上一篇咱們經過簡單的ArrayList的源碼分析,分析了ArrayList爲何有序,爲何讀操做快?爲何寫操做慢?詳情請穿越 jdk1.8-Java集合框架之二-ArrayList源碼分析node

這篇咱們講解的是LinkedList,一樣咱們是帶着問題來的:數組

  1. LinkedList爲何是有序的,它的數據結構是什麼??
  2. LinkedList爲何是讀效率低,寫效率高呢??
  3. LinkedList中可否存在重複的值??是否容許添加null值??

循序漸進

套路一(先看長相):先看LinkedList的類繼承關係圖。

能夠看出,LinkedList繼承了一個父類AbstractSequentialList,另外實現了4個接口,分別是Cloneable、List、Deque、Serializable。bash

父類AbstractSequentialList是一個抽象類,提供部分已經實現的功能,單與本次講解的LinkedList內容關係不大,暫且不談,有興趣的同窗能夠自行了解,數據結構

回想一下ArrayList的源碼,發現ArrayList實現的接口與LinkedList實現的接口差異就在於ArrayList實現了RandomAccess接口而LinkedList沒有,LinkedList實現了Deque接口而ArrayList沒有。框架

what?Deque接口又是什麼鬼??dom

public interface Deque<E> extends Queue<E> 
複製代碼

Deque接口是Queue接口的子接口,且從名字上咱們能夠猜出,應該是雙向隊列。源碼分析

從繼承關係圖能夠得出兩個結論,1:LinkedList沒有實現RandomAccess接口說明LinkedList不支持快速訪問;2.LinkedList實現了Deque接口說明是一個雙向隊列的數據結構。post

套路二(再看身材):再看LinkedList定義的屬性

//長度
transient int size = 0;
//指向第一個節點的指針
transient Node<E> first;
//指向最後一個節點的指針
transient Node<E> last;
複製代碼

就這樣完了??
LinkedList只定義了三個實例變量,沒有其餘的了。既然這樣,那咱們就先撩一下看這個Node是哪一個小妖精。。ui

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範型一致的item,另外兩個屬性類型都是Node,next指向Node對象的下一個Node對象,prev指向Node對象的前一個Node對象。this

很明顯,Node類這樣的設計,咱們能夠回答一開始帶來的第一個問題,LinkedList就是一個鏈表的數據結構設計,每個對象都持有指向前一個對象的引用,也都持有指向後一個對象的引用,這種結構天生就是有序的。

套路三(看內在):看具體的實現

先從構造方法入手,LinkedList提供了兩個構造方法,無參數的構造方法沒有任何實現,咱們直接看有參數的構造方法:

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

該構造方法須要傳入一個Collection集合實現類對象,直接將集合對象放進addAll方法裏,咱們再看allAll方法:

public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
複製代碼

addAll方法又調用了addAll的重載方法,並傳入了兩個參數,第一個是LinkedList的實例變量size,第二個是構造方法傳進來的Collection集合對象。繼續看addAll的這個重載方法:

public boolean addAll(int index, Collection<? extends E> c) {
    //檢查index是否合法
    checkPositionIndex(index);
    //將Collection實例轉爲Object數組
    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)
        return false;

    Node<E> pred, succ;
    //index等於size時,只有兩種狀況:
    //1.LinkedList中沒有元素
    //2.要添加的元素位置等於LinkedList長度
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        //尋找index位置存在的元素
        succ = node(index);
        //找到index位置存在的元素的前一個元素
        pred = succ.prev;
    }

    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        //將對象包裝成一個Node對象
        Node<E> newNode = new Node<>(pred, e, null);
        if (pred == null)
            //若是index位置前一個元素爲null,表示當前添加的是第一個元素
            //將當前元素的包裝對象設置爲第一個
            first = newNode;
        else
            //不然設置index位置前一個元素的下一個元素爲當前新添加的元素
            pred.next = newNode;
        //添加完成,須要將pred設置爲新添加的元素
        pred = newNode;
    }

    if (succ == null) {
        //若是添加前長度爲0
        //或者添加元素的位置在LinkedList長度以外
        //那麼LinkedList最後元素應該是最後添加的元素
        last = pred;
    } else {
        //否者表示在已有元素中間添加元素
        //那麼最後添加的元素的下一個元素應該是添加前index位置對應的元素
        pred.next = succ;
        //添加前index位置的元素,設置前一個元素爲最後添加的元素
        succ.prev = pred;
    }
    //LinkedList長度增長
    size += numNew;
    modCount++;
    return true;
}
複製代碼

核心代碼的邏輯相對ArrayList複雜了點,但也是比較簡單的。其實整個LinkedList的結構就像俄羅斯套娃,每層套娃都用線連着外面和裏面的套娃。

那麼針對第二個問題,LinkedList爲何是讀效率低,寫效率高呢,咱們接着往下看:

public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}
Node<E> node(int index) {
    //由於LinkedList實現了Deque接口,因此LinkedList仍是雙向隊列
    //因此此處查找元素,最大的查找範圍是最大長度的1/2
    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;
    }
}
複製代碼

還記得ArrayList的讀操做嗎,是經過數組下標直接找到對應元素,複雜度是O(1),而LinkedList是鏈表結構的,並無下標,因此只能從頭至尾遍歷,或者從尾到頭遍歷查找,複雜度是O(n),因此LinkedList相對ArrayList來講,讀效率很低。

那麼爲何又說寫效率高呢??咱們接着看:

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

    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}
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++;
}
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++;
}
複製代碼

上面代碼不難看出,LinkedList添加元素,其實只須要設置添加的元素的先後元素引用,並將先後元素的相關屬性設置爲所須要添加的元素的引用,就完成了元素的添加。同理,刪除元素,只須要移除元素,並將元素的先後元素相關屬性從新設置,則完成刪除操做。整個LinkedList的單個元素寫操做,最多涉及3個元素的修改。

相對ArrayList添加或者刪除元素都須要從新計算容量甚至擴容,再將部分甚至所有元素複製再移位的操做來講,LinkedList的寫操做效率實在高太多。

對於最後一個問題,講到目前爲止,相信不用說你們都知道了。LinkedList容許添加劇復的值,也容許添加null值,由於LinkedList並非直接維護添加的元素,而是將添加的元素包裝成Node對象來維護,因此添加什麼元素都無所謂了。

過後回味

LinkedList:有序、雙向鏈表、可重複、讀效率低、寫效率高。
適用場景:業務頻繁寫操做。

Java集合框架之LinkedList就簡單講到這,下節講HashSet,有不一樣意見或建議歡迎指出,謝謝。

相關文章
相關標籤/搜索