JDK8中LinkedList的工做原理剖析

LinkedList雖然在平常開發中使用頻率並非不少,但做爲一種和數組有別的數據結構,瞭解它的底層實現仍是頗有必要的。java

在這以前咱們先來複習下ArrayList的優缺點,ArrayList基於數組的動態管理實現的,數組在內存中是一塊連續的存儲地址而且數組的查詢和遍歷是很是快的;缺點在於在添加和刪除元素時,須要大幅度拷貝和移動數據,還要考慮是否須要擴容操做,因此效率比較低。node

正是因爲上面的不足,纔出現了鏈表的這種數據結構,首先鏈表在內存中並非連續的,而是經過引用來關聯全部元素的,因此鏈表的優勢在於添加和刪除元素比較快,由於只是移動指針,而且不須要判斷是否擴容,缺點是查詢和遍歷效率比較低下。數組

首先,咱們先看下LinkedList的繼承結構:安全

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

從上面的源碼裏面能夠看到:網絡

LinkedList繼承了AbstractSequentialList是一個雙向鏈表,能夠當作隊列,堆棧,或者是雙端隊列。數據結構

實現了List接口能夠有隊列操做。併發

實現了Deque接口能夠有雙端隊列操做函數

實現了Cloneable接口既能夠用來作淺克隆工具

實現了Serializable接口能夠用來作網絡傳輸和持久化,同時可使用序列化來作深度克隆。性能

下面咱們先來看下LinkedList的核心數據結構:

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

而後在看下其成員變量和構造方法:

`   transient int size = 0;//當前存儲元素的個數

    transient Node<E> first;//首節點

    transient Node<E> last;//末節點

    //無參數構造方法 
    public LinkedList() {
    }

   //有參數構造方法
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

從上面能夠看到LinkedList有兩個構造函數,一個無參,一個有參,有參的構造函數的功能是經過一個集合參數並把它裏面全部的元素給插入到LinkedList中,注意這裏爲何說是插入,而不是說初始化添加,由於LinkedList並不是線程安全,徹底有可能在this()方法調用以後,已經有其餘的線程向裏面插入數據了。

(一)addAll方法分析

下面咱們看下addAll方法的調用鏈:

`   //1
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
  //2
    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;
        //說明是尾部插入
        if (index == size) {
            succ = null;//後置節點同時爲最後一個節點的值老是爲null
            pred = last;//前置節點是當前LinkList節點的最後一個節點
        } else {//非尾部插入
            succ = node(index);//找到index節點做爲做爲後置節點
            pred = succ.prev;//index節點的前置節點賦值當前的前置節點
        }

        //遍歷對象數組
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;//轉爲指定的泛型類
            //當前插入的第一個節點的初始化
            Node<E> newNode = new Node<>(pred, e, null);
            //若是節點爲null,那麼說明當前就是第一個個節點
            if (pred == null)
                first = newNode;
            else//不爲null的狀況下,把pred節點的後置節點賦值爲當前節點
                pred.next = newNode;
            //上面的操做完成以後,就把當前的節點賦值爲臨時的前置節點
            pred = newNode;
        }
        //循環追加完畢後
        //判斷若是是尾部插入
        if (succ == null) {//若是最後一個節點是null,說明是尾部插入,那麼尾部節點的前置節點,就會被賦值成最後節點
            last = pred;
        } else {//非尾部插入
            pred.next = succ;//尾部節點等於succ也就是index節點
            succ.prev = pred;//succ也就是index節點的前置節點,就是對象數組裏面最後一個節點
        }
        //size更新
        size += numNew;
        modCount++;//修改次數添加
        return true;
    }


    
    //3 給一個index查詢該位置上的節點
    Node<E> node(int index) {
        // assert isElementIndex(index);
         //若是index小於當前size的一半,就從首節點開始遍歷查找
        if (index < (size >> 1)) {
            Node<E> x = first;//初始化等於首節點
            for (int i = 0; i < index; i++)
                x = x.next;//依次向後遍歷index次,並將查詢結果返回
            return x;
        } else {//不然,就從末尾節點向前遍歷
            Node<E> x = last;//初始化等於末尾節點
            for (int i = size - 1; i > index; i--)
                x = x.prev;//依次向前遍歷index次,並將查詢結果返回
            return x;
        }
    }

這裏面咱們看到主要方法有兩個:

首先是addAll(int index, Collection c)方法,這裏面先判斷了是否會出現索引越界的可能,而後分別初始化了兩個臨時節點pred和succ,分別做爲index節點的前置節點和後置節點,若是不是在第一次初始化插入的狀況下,這段代碼的工做原理,你們能夠理解爲一個木棒一刀兩斷以後,第一段的末尾處就是前置節點,而第二段木棒的開始處就是後置節點,咱們插入的數據就相似於放在兩個木棒之間,而後在依次追加進來,最後把前置節點鏈接上和後置節點鏈接上,就至關於插入完成,變成了一根更長的木棒,這個過程你們用筆畫一下,仍是比較容易理解的。

而後你們看到還有一個方法node(int index),這個方法的主要功能是找到index個數上的Node節點,雖然源碼裏面已經作過遍歷優化,折半查詢,若是index小於size的一半,就從頭開始向後遍歷查詢,不然就從後向前遍歷查詢,即便這樣,遍歷和查詢仍然是鏈表的缺點,能夠當作是O(n)操做。

(二)add方法分析

add方法無疑是操做鏈表常常用的方法,它的源碼以下:

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

//2
    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++;//size更新
        modCount++;
    }

從上面咱們看到add方法每次都會把新增節點放在鏈表的最後的一位,正是由於放在鏈表的末位,因此鏈表的添加性能能夠當作O(1)操做。

(二)remove方法分析

移除方法有經常使用的有兩個,一個是根據index移除,另一個根據Object移除,源碼以下:

//1
    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    
    //2
        public boolean remove(Object o) {
        //移除的數據爲null
        if (o == null) {
            //遍歷找到第一個爲null的節點,而後移除掉
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            //移除的數據不爲null,就遍歷找到第一條相等的數據,而後移除掉
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }
    
    
    //3
        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;

        //若是前置節點爲null,那麼後置節點就賦值給首節點
        if (prev == null) {
            first = next;
        } else {//前置節點的後置節點爲當前節點的後置節點
            prev.next = next;
            x.prev = null;//當前節點的前置節點置位null
        }
        //若是後置節點爲null,末尾節點就爲當前節點的前置節點
        if (next == null) {
            last = prev;
        } else {//不然後置節點的前置節點爲移除自己的前置節點
            next.prev = prev;
            x.next = null;//移除節點的末尾爲null
        }
        //移除的數據置位null,便於gc
        x.item = null;
        size--;
        modCount++;
        return element;
    }

從上面的源碼裏能夠看到移除根據index移除裏面調用了node(index)方法來查找須要移除的節點,而根據Object移除的時候,則是進行了整個鏈表的遍歷,而後再卸載節點。

除此以外鏈表還有沒有任何參數的remove,removeFirst,removeLast方法,其中remove方法本質是調用了removeFirst方法。

這裏可以總結出來鏈表基於首尾節點的刪除能夠當作是O(1)操做,而非首尾的刪除最壞的狀況下可以達到O(n)操做,由於要遍歷查詢指定節點,因此性能較差。

(三)get方法分析

get系方法有三個,分別是get(index),getFirst(),getLast(),其中get(index)方法以下:

public E get(int index) {
        checkElementIndex(index);//是否越界
        return node(index).item;//折半遍歷查詢
    }

咱們看到get(index)方法本質調用了node(index)方法,這個方法在前面分析過了性能一半,其餘的getFirst和getLast不用多說了O(1)操做。

(四)set方法分析

源碼以下:

public E set(int index, E element) {
        checkElementIndex(index);//是否越界
        Node<E> x = node(index);//折半查詢
        E oldVal = x.item;// 查詢舊值
        x.item = element;//放入新值
        return oldVal;//返回舊值
    }

set方法依舊是調用的node方法,因此鏈表在指定位置更新數據,性能也通常。

(四)clear方法分析

public void clear() {
         //遍歷全部的數據,置位null      
        for (Node<E> x = first; x != null; ) {
            Node<E> next = x.next;
            x.item = null;
            x.next = null;
            x.prev = null;
            x = next;
        }
        first = last = null;
        size = 0;
        modCount++;
    }

clear方法比較簡單,就是全部的節點的數據置位null,方便垃圾回收

(五)toArray方法分析

public Object[] toArray() {
      //聲明長度同樣的數組
        Object[] result = new Object[size];
        int i = 0;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        return result;
    }

聲明一個長度同樣的數組,依次遍歷全部數據放入數組。

(六)序列化和反序列方法分析

//序列化
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        //先寫入大小
        s.writeInt(size);
        //再依次遍歷鏈表寫入字節流中
        for (Node<E> x = first; x != null; x = x.next)
            s.writeObject(x.item);
    }
    
    //反序列化
        private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

         //先讀取大小
        int size = s.readInt();
        //再依次讀取元素,每次都追加到鏈表末尾
        for (int i = 0; i < size; i++)
            linkLast((E)s.readObject());
    }

這裏咱們看到鏈表中也自定義了序列化和反序列化的方法,在序列化時只寫入x.item而並非整個Node,這樣作避免java自帶的序列化機制會把整個Node的數據給寫入序列化,而且若是Node仍是雙端鏈表的數據結構,那麼無疑會致使重複兩倍的空間浪費。

在反序列化時咱們看到先讀取size,而後根據size依次循環讀取item,並從新生成雙端鏈表的數據結構,依次追加到鏈表的尾部。

(六)關於操做隊列或者堆棧的方法

文章開頭說了LinkedList能夠當雙端隊列或者堆棧使用,這裏面有一系列的方法,這裏只簡單列舉幾個經常使用的方法,由於原理比較簡單因此不在敘述:

pop()//移除第一個元素並返回
push(E e)//添加一個元素在隊列首位
poll()  //移除第一個元素
offer(E e)//添加一個元素在隊列末位
peek()//返回第一個元素,沒有移除動做

總結:

本文介紹了JDK8中LinkedList的工做原理,並對其經常使用的方法進行了分析,LinkedList底層是一個鏈表,鏈表在內存中不是一塊連續的地址,而是用多少就會申請多少,因此它比ArrayList更加節省空間, 此外它的添加方法和首末位的刪除操做很是快,可是查詢和遍歷操做比較耗時。最後LinkedList還能夠用來當作雙端隊列和堆棧容器,須要特別注意的是LinkedList也並不是是線程安全的,若是須要 請使用其餘併發工具包下提供的類。

相關文章
相關標籤/搜索