LinkedList源碼分析--基於JDK1.8

LinkedList源碼分析

LinkedList的UML圖:
1592201271(1).png
LinkedList真正用來存儲元素的數據結構-> Node類
Node類是LinkedList中的私有內部類,LinkedList中經過Node來存儲集合中的元素node

  • E:節點的值
  • Node next: 當前節點的後一個節點的引用(能夠理解爲指向當前節點的後一個節點的指針)
  • Node prev:當前節點的前一個節點的引用(能夠理解爲指向當前節點的前一個節點的指針)
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;
    }
}

LinkedList的元素

//LinkedList節點個數,用來記錄LinkedList的大小
transient int size = 0;

/**
 * 指向第一個節點的指針。用來表示LinkedList的頭結點
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * 指向最後一個節點的指針。用來表示LinkedList的尾結點
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;

LinkedList的構造函數(兩個)

ArrayList的構造函數有三個,比LinkedList多提供了一個設置初始化容量來初始化類。LinkedList沒有提供該方法,緣由:由於LinkedList底層是經過鏈表實現的,每添加新元素的時候,都是經過連接新的節點實現的,也就是說它的容量是隨着元素的個數的變化而動態變化的。而ArrayList底層是經過數組來存儲新添加的元素的,因此咱們能夠爲ArrayList設置初始容量(也就是設置數組的大小)數組

/**
 * 空參構造
 */
public LinkedList() {
}

/**
 * 構造包含指定集合元素的列表,按照集合的迭代器返回元素的順序
 * 傳入一個結合做爲參數初始化LinkedList。
 *
 * @param  c 將其元素放置在此列表中的集合
 * @throws NullPointerException 若是指定的集合爲空
 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList的構造函數(帶參數)

在LinkedList帶集合參數的構造函數中有一個重要的方法addAll(int index, Collection),這塊兒的方法有點繞,若是沒看懂,建議手畫一遍。數據結構

/**
 * 經過調用addAll(int index, Collection<? extends E> c)添加 集合
 */
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}
/**
 * checkPositionIndex(index); 檢查傳入的參數是否合法
 */
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);
    // 將集合轉換爲數組
    Object[] a = c.toArray();
    // numNew 存儲數組的長度
    int numNew = a.length;
    // 若是 c:待添加的集合爲null,直接返回false,不進行後面的操做
    if (numNew == 0)
        return false;
    // pred:指代 待添加節點的前一個節點
    // succ:指的是待添加節點的位置
    Node<E> pred, succ;
     // 若是index==size,說明此時須要添加LinkedList的集合中每個元素都是在LinkedList最後面。因此把succ設置爲null, pred指向尾結點
     // 不然的話succ指向插入待插入位置的節點。這裏用到了node(index)方法,pred指向succ節點的前一個節點
    if (index == size) {
        // 新添加的元素的位置是位於LinkedList最後一個元素的後面,也就是在LinkedList尾部追加元素
        succ = null;
        pred = last;
    } else {
        succ = node(index);
        pred = succ.prev;
    }
    /**
      * 遍歷數組中的每個元素。在每次遍歷的時候,都新建一個節點,該節點的值存儲數組a中遍歷的值,該節點的prev存儲pred節點,next設置爲null。
      * 接着判斷該節點的前一個節點是否爲空,若是爲空的話,則把當前節點設置爲頭結點
      * 不然的話就把當前節點的前一個節點的next值設置爲當前節點。最後把pred指向當前節點,方便後續節點的添加
      */
    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;
    }
    /**
      * 當succ爲null時,也就是新添加的節點位於LinkedList集合的最後一個元素的後面。
      * 上面的for遍歷的a的全部元素,此時的pred指向的是Linked中的最後一個元素,因此把last指向pred指向的節點
      * 當不爲空的時候,代表在LinkedList集合中添加的元素,須要把pred的next指向succ上,succ的prev指向pred
      */
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }
    // 從新設置集合的大小
    size += numNew;
    // 修改的次數-自增。
    modCount++;
    return true;
}

LinkedList中的一些輔助方法

  • linkFirst(E e){};把參數中的元素做爲鏈表的第一個元素
/**
 * 連接e做爲第一個元素。
 */
private void linkFirst(E e) {
    final Node<E> f = first;// LinkedList中的第一個元素first賦給 f
    // 組建新的node,新添加的元素的succ(後指針指向給 f)
    final Node<E> newNode = new Node<>(null, e, f);
    // 將新插入的節點e,賦給first
    first = newNode;
    // 若是 f== null ;說明是空鏈表,把新節點設置爲尾結點
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}
  • linkLast(E e){};把參數中的元素做爲鏈表的最後一個元素
/**
 * 連接e做爲最後一個元素。
 */
void linkLast(E e) {
    // 獲取尾部元素
    final Node<E> l = last; 
    // 以尾部元素爲前繼結點建立一個新節點
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾部節點爲須要插入的節點
    last = newNode;

    if (l == null)
        //若是爲空鏈表的狀況:同時更新first節點爲須要插入的節點。(也就是說,該節點便是頭結點也是尾節點last)
        first = newNode;
    else
        // 不是空鏈表的狀況:將原來的尾部節點(如今是卻是第二個節點)的next指向須要插入的節點
        l.next = newNode;
    size++;  // 更新鏈表大小和修改次數,插入完畢
    modCount++;
}
  • linkBefore(E e,Node succ);在非空節點succ以前插入元素 e
/**
 * 將元素e插入到非空節點succ以前。
 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    // 建立一個pred變量指向succ節點的前一個節點
    final Node<E> pred = succ.prev;
    // 建立一個新節點,他的prev設置爲咱們新建的pred變量,後節點設置爲succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    // succ的上節點(prev)指向新建的節點
    succ.prev = newNode;
    // 判斷succ的前節點是否爲null,爲null,則把新節點設置爲鏈表的頭結點
    if (pred == null)
        first = newNode;
    else
        // 不爲空把,succ的前一個節點的next指向新節點
        pred.next = newNode;
    size++;
    modCount++;
}
  • unlinkFirst( Node f ){};刪除LinkedList中的第一個節點。(該節點不爲空,返回刪除節點的值)。這是一個私有方法,咱們不可以調用,assert f == first && f != null; 參數f是頭結點,並且f不能爲null
/**
 * 解除非空第一個節點f的連接。
 */
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    // 定義一個變量element,值爲待刪除節點的值。
    final E element = f.item;
    // 定義變量next,值爲:待刪除的節點的下一個節點
    final Node<E> next = f.next;
    // f節點的值設置爲空
    f.item = null;
    f.next = null; // help GC
    // 將變量next設置爲頭節點
    first = next;
    // 判斷f的下一個節點是否爲空,爲空:把last設置爲空
    if (next == null)
        last = null;
    else
        // 不爲空,將next的前節點設置爲空。next爲頭結點
        next.prev = null;
    size--;
    modCount++;
    return element;
}
  • unlinkLast(Node l){};刪除LinkedList的最後一個節點(該節點不爲空,並返回刪除節點對應的值)
/**
 * Unlinks non-null last node l.
 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;
    // 建立變量element,值爲:待刪除的節點的值
    final E element = l.item;
    // 建立變量 prev:值爲:待刪除的節點的前節點
    final Node<E> prev = l.prev;
    // 待刪除的節點的值賦空
    l.item = null;
    l.prev = null; // help GC
    // 將last指向 新建的節點 prev 
    last = prev;
    // 判斷待刪除的節點的前節點是否爲空,爲空:該鏈表則爲空鏈表,將頭結點first賦null值
    if (prev == null)
        first = null;
    else
        // 不爲空,將待刪除的前節點的next指向 null
        prev.next = null;
    size--;
    modCount++;
    return element;//返回刪除的節點的值
}
  • unlink(Node e);刪除一個節點,該節點不爲空
/**
 * Unlinks non-null node x.
 */
E unlink(Node<E> x) {
    // assert x != null;
    // 變量 element:值:要刪除的節點的值
    final E element = x.item;
    // 新建變量:next,值:要刪除的節點的下一個節點
    final Node<E> next = x.next;
    // 新建變量:prev,值:要刪除的節點的上一個節點
    final Node<E> prev = x.prev;
    // 判斷要刪除的節點的上一個節點爲空,爲空:則刪除的是頭結點,將first指向新建的next
    if (prev == null) {
        first = next;
    } else {
        // 不爲空:將要刪除的節點的上一個節點的next指向要刪除的節點的下一個節點
        prev.next = next;
        x.prev = null;
    }
    // 判斷要刪除的節點的下一個節點是否爲空,爲空:則刪除的尾結點,將last指向prev,也就是指向要刪除的節點的上一個節點
    if (next == null) {
        last = prev;
    } else {
        // 不爲空,將要刪除的節點的下一個節點的上節點指向-》要刪除的節點的上個節點
        next.prev = prev;
        x.next = null;
    }
    // 這一步就把要刪除的節點賦了空值,有助於gc回收
    x.item = null;
    size--;
    modCount++;
    return element;
}
  • Node node(int index); 計算指定索引上的節點,並返回。 這裏LinkedList不是從頭開始進行遍歷,而是先比較一下index更靠近鏈表的頭結點仍是尾結點,而後進行遍歷,獲取對應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;
    }
}

以上是一些輔助方法,在LinkedList的add,get,remove等方法中都會使用到相應的方法。函數

LinkedList中的contains(Object o);

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

indexOf(Object o)方法中分兩種狀況,源碼分析

  1. 首先判斷傳入的參數 o 是否是空,測試

    1. 爲空:for循環進行查找,找第一個節點的item的值==null的,找到返回對應的下標,沒有則返回-1
    2. 不爲空,for循環進行查找,找第一個節點的item的值與o相等的,找到返回對應的下標,沒有返回-1;

我的認爲看懂了上面的方法,其餘的方法都簡單明瞭。優化

LinkedList和ArrayList的區別

  • 順序插入的速度ArrayList會快些,LinkedList的速度會稍慢一些。由於ArrayList只是在指定的位置上賦值便可,而LinkedList則須要建立Node對象,而且須要創建先後關聯,若是對象比較大的話,速度會稍微慢一些
  • LinkedList佔用的內存空間要大一些
  • 數組遍歷的方式ArrayList推薦使用for循環,而LinkedList則推薦使用foreach,若是使用for循環,效率將會很慢。下面有測試代碼
  • LinkedList作插入、刪除的時候,慢在尋址,快在只須要改變先後Entrty的引用地址;ArrayList作插入、刪除的時候,慢在數組元素的批量copy,快在尋址。

因此,若是待插入、刪除的元素是在數據結構的前半段,尤爲是很是靠前的位置的時候,LinkedList的效率將大大快過ArrayList,由於ArrayList將批量copy大量的元素;越日後,對於LinkedList來講,由於它是雙向鏈表,因此在第2個元素後面和在倒數第2個元素後面插入一個元素在效率上基本沒有差異,可是ArrayList因爲copy的元素愈來愈少,操做速度確定會愈來愈快this

爲何不建議使用for循環迭代LinkedList

測試:spa

步驟:一、分別建立一個LinkedList和一個ArrayList;
    二、分別插入100000條數據
    三、分別使用for,foreach。iterator遍歷,記錄所用時間
LinkedList<String> linkedList = new LinkedList<String>();

ArrayList<String> arrayList = new ArrayList<String>();

for(int i = 0; i < 100000; i ++) {
    linkedList.add("linkedList -- " + i);
    arrayList.add("arrayList -- " + i);
}

// ------------------------foreach-----------------------
long befor = System.currentTimeMillis();
for(String ii : arrayList){
}
long after = System.currentTimeMillis();
System.out.println("Arraylist使用foreach遍歷的時間是:"+(after-befor)+"ms");

befor = System.currentTimeMillis();
for(String ii : linkedList){
}
after = System.currentTimeMillis();
System.out.println("Linkedlist使用foreach遍歷的時間:"+(after-befor)+"ms");
// ------------------------Iterator-----------------------
Iterator<String> arrayListIterator = arrayList.iterator();
befor = System.currentTimeMillis();
while(arrayListIterator.hasNext()) {
    arrayListIterator.next();
}
after = System.currentTimeMillis();
System.out.println("Arraylist使用Iterator遍歷的時間是:"+(after-befor)+"ms");


Iterator<String> linkedListIterator = arrayList.iterator();
befor = System.currentTimeMillis();
while(linkedListIterator.hasNext()) {
    linkedListIterator.next();
}
after = System.currentTimeMillis();
System.out.println("Linkedlist使用Iterator遍歷的時間是:"+(after-befor)+"ms");
// -------------------------for----------------------------

befor = System.currentTimeMillis();
for (int i = 0; i < arrayList.size(); i++) {
    arrayList.get(i);
}
after = System.currentTimeMillis();
System.out.println("Arraylist使用for遍歷的時間是:"+(after-befor)+"ms");



befor = System.currentTimeMillis();
for (int i = 0; i < linkedList.size(); i++) {
    linkedList.get(i);
}
after = System.currentTimeMillis();
System.out.println("Linkedlist使用for遍歷的時間是:"+(after-befor)+"ms");


// 第1次運行結果
/**
  * Arraylist使用foreach遍歷的時間是:7ms
  *  Linkedlist使用foreach遍歷的時間:10ms
  * Arraylist使用Iterator遍歷的時間是:2ms
  * Linkedlist使用Iterator遍歷的時間是:1ms
  * Arraylist使用for遍歷的時間是:0ms
  * Linkedlist使用for遍歷的時間是:35070ms
  */
// 第2次運行結果
/**
  * Arraylist使用foreach遍歷的時間是:2ms
  *  Linkedlist使用foreach遍歷的時間:4ms
  * Arraylist使用Iterator遍歷的時間是:1ms
  * Linkedlist使用Iterator遍歷的時間是:1ms
  * Arraylist使用for遍歷的時間是:0ms
  * Linkedlist使用for遍歷的時間是:37262ms
  */

10萬條記錄。能夠看出,不論是foreach仍是Iterator,速度都差異不大,可是對於for循環時,linkedList的for循環遍歷的時間很長指針

緣由分析

LinkedList的get();

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

因爲LinkedList是雙鏈表,因此經過index去獲取的時候回判斷index是在前半段仍是後半段,前半段正序遍歷,後半段倒序遍歷,這也是Linked優化的部分。那麼爲何使用不一樣的for循環遍歷LinkedList會很慢呢?
例:

  • get(0):直接拿到Node(0)的地址,獲取裏面的數據
  • get(1):先拿到Node(0)的地址,在從Node(0)中獲取到Node(1)的地址,在拿到Node(1)的數據
  • get(2):先拿到Node(0)的地址,在從Node(0)中獲取Node(1)的地址,在從Node(1)中獲取Node(2)的地址,在獲取到Node(2)的數據

以此類推,也就是說,LinkedList在get任何數據的時候,會把前面的數據走一遍,隨着LinkedList的容量愈來愈大,時間的消耗就會愈來愈長 因此說建議使用迭代器或者foreach循環去遍歷LinkedList,這種方式是直接按照地址去找數據的,將大大提高遍歷LinkedList的效率。

相關文章
相關標籤/搜索