知其然知其因此然之LinkedList經常使用源碼閱讀

Hello你們好,本章咱們簡單瞭解一下LinkedList 。有問題能夠聯繫我mr_beany@163.com。另求各路大神指點,感謝。

說明:本篇文章基於jdk1.8進行閱讀,並針對LinkedList中經常使用的一些方法進行簡要說明。java

一:LinkedList簡介

LinkedList是一種鏈表類型的數據結構,支持高效的插入和刪除操做。其實現了 Deque 接口,使得 LinkedList具備隊列的特性。LinkedList 類的底層實現的數據結構是一個雙端的鏈表。node

二:數據結構圖分析


如圖能夠看出LinkedList數據結構使用雙向鏈表結構,有一個頭節點和一個尾節點,第一個節點的前驅節點爲null,最後一個節點的後繼節點爲null。其中每一個節點有兩個指針指向前驅節點和後繼節點,這意味着咱們在添加或修改LinkedList時沒必要像ArrayList同樣進行擴容操做。數組

三:繼承關係分析

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable複製代碼
  • 繼承於 AbstractSequentialList。而AbstractSequentialList這個類提供了一個基本的 List 接口實現,爲實現序列訪問的數據儲存結構的提供了所須要的最小化的接口實現。他採用的是在迭代器的基礎上實現的 get、set、add 和 remove 方法。
  • 實現List接口,說明能夠對他進行隊列的操做
  • 實現Deque接口,能夠將 LinkedList 看成雙端隊列使用
  • 實現Cloneable接口,說明能夠進行克隆
  • 實現Serializable接口,說明能夠被序列化

四:源碼分析

1:成員變量安全

//transient修飾的變量在序列化時不會被序列化   
transient int size = 0;

/**
 * Pointer to first node.
 * Invariant: (first == null && last == null) ||
 *            (first.prev == null && first.item != null)
 */
transient Node<E> first;

/**
 * Pointer to last node.
 * Invariant: (first == null && last == null) ||
 *            (last.next == null && last.item != null)
 */
transient Node<E> last;複製代碼

LinkedList的成員變量很簡單,只有三個。其中size表示鏈表中實際元素的數量。bash

根據註釋結合上面的數據結構圖能夠看出:數據結構

  • 當鏈表爲空時,first和last必定都爲空。
  • 當鏈表不爲空時,first的前驅結點必定爲空,first.item必定不爲空。last的後續節點必定爲空,last.item必定不爲空。

2:內部類函數

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;
    }
}複製代碼

3:構造方法工具

//構建一個空列表
public LinkedList() {
}

/**
 * 構建一個包含集合c的列表
 *
 * @param  c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */ 
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}複製代碼

4:經常使用方法源碼分析

  • 添加元素到第一個節點

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++;
}複製代碼

分析,經過變量f存儲第一個節點,接下來調用Node建立一個新的節點,將新節點做爲新first,這時判斷,若是以前的first爲空,那說明插入以前該鏈表是空的,那麼新插入的節點不只是first節點並且仍是last節點,因此last要指向新插入的 newNode。優化

若是以前的first不是空,那麼不動last,將first的前驅結點設爲新節點,此時原first節點爲鏈表的第二個節點。

最後插入成功,將鏈表節點數量加一

說明:Fail-Fast 機制

變量modCount爲記錄當前鏈表被修改的次數。咱們知道LinkedList是線程不安全的,在迭代器遍歷鏈表時,迭代器初始化過程當中會將這個值賦給迭代器的expectedModCount。在迭代過程當中,判斷modCount跟expectedModCount 是否相等,若是不相等就表示已經有其餘線程修改了。那麼將拋出 ConcurrentModificationException。因此當你們遍歷那些非線程安全的數據結構時,儘可能使用迭代器進行遍歷

重點:向鏈表中添加元素

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

//添加指定集合到指定位置
public boolean addAll(int index, Collection<? extends E> c) {
        //判斷須要插入的位置是否合法  index>=0且index小於等於當前鏈表節點數量
        checkPositionIndex(index);
        //將集合轉換爲數組
        Object[] a = c.toArray();
        //保存集合長度
        int numNew = a.length;
        if (numNew == 0)
            return false;

        //建立節點  前驅節點,後續節點
        Node<E> pred, succ;
        //若是插入的節點爲鏈表的末端,則前驅節點爲尾節點,後續節點爲空
        if (index == size) {
            succ = null;
            pred = last;
        } else {
            //根據節點位置獲取該節點  下面分析
            succ = node(index);
            //保存該節點的前驅節點,這裏咱們將鏈表斷開,準備將集合插入指定位置  succ保存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;
            //將前驅改爲當前節點,以便後續添加c中其它的元素
            pred = newNode;
        }

        //仍是兩種判斷 若是succ爲空,說明插入的節點位置是該鏈表的尾節點的後面
        //這時經過上面的遍歷咱們能夠知道,pred確定指向當前鏈表最後一個節點,因此將last指向pred
        if (succ == null) {
            last = pred;
        } else {
            //此時咱們須要將以前斷開的鏈表的後半部分拼接上。pred爲當前重組的尾節點,
            //則尾節點的後續節點指向succ,succ的前驅節點指向pred
            pred.next = succ;
            succ.prev = pred;
        }
        //鏈表長度增長
        size += numNew;
        //操做次數增長
        modCount++;
        return true;
    }複製代碼

獲取指定位置的節點

//這裏咱們能夠看到對此查找,LinkedList是作了優化的,
//並無盲目的去所有遍歷,而是判斷要查找的座標離頭節點近仍是尾節點近,來判斷是從頭遍歷仍是從尾開始遍歷
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;
    }
}複製代碼

刪除一個元素(元素不爲空

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;
}複製代碼

刪除 LinkedList 中第一個節點(私有

//根據註釋咱們能夠看出元素必須是第一個元素且不能爲空
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //存儲待刪除節點的值,用做刪除成功返回帶=待刪除節點的值
    final E element = f.item;
    //存儲待刪除節點的後續節點
    final Node<E> next = f.next;
    //把f的值和它的next設置爲空
    f.item = null;
    f.next = null; // help GC
    //將他的下一個節點設置爲頭節點
    first = next;
    //判斷next是否爲空,若是爲空證實原鏈表只有一個節點,刪除以後是空鏈表
    if (next == null)
        //將last也須要設置爲空號
        last = null;
    else
        //這是next已經爲頭節點,須要將頭節點的前驅節點設置爲空
        next.prev = null;
    //鏈表元素數量減一
    size--;
    //操做次數加一
    modCount++;
    return element;
}複製代碼

刪除 LinkedList 的最後一個節點。(該節點不爲空

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; // help GC
        //將他的下一個節點設置爲尾節點
        last = prev;
        //判斷prev是否爲空,若是爲空證實原鏈表只有一個節點,刪除以後是空鏈表
        if (prev == null)
            first = null;
        else
            //將尾節點的後續節點設置爲空
            prev.next = null;
        size--;
        modCount++;
        return element;
    }複製代碼

工具函數

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

    /**
     * Removes and returns the last element from this list.
     *
     * @return the last element from this list
     * @throws NoSuchElementException if this list is empty
     */
    public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }複製代碼

五:總結

元素源碼咱們能夠看出LinkedList在添加刪除元素時,不須要像ArrayList同樣去擴容。而在根據元素所在位置去查找元素時又不像ArrayList中同樣直接去取,而是須要遍歷。因此LinkedList更多的適用於頻繁添加刪除,而查找很少的場景下。

根據位置查找元素時先判斷離頭節點近仍是尾節點近這種優化方式咱們也能夠用於平常的代碼中

LinkedList是線程不安全的,在遍歷的過程當中咱們儘可能使用迭代器進行遍歷

相關文章
相關標籤/搜索