JDK1.8之LinkeList以及與ArrayList的區別

二、LinkedList數據結構

  還是老規矩,先抓住LinkedList的核心部分:數據結構,其數據結構如下

  說明:如上圖所示,LinkedList底層使用的雙向鏈表結構,有一個頭結點和一個尾結點,雙向鏈表意味着我們可以從頭開始正向遍歷,或者是從尾開始逆向遍歷,並且可以針對頭部和尾部進行相應的操作。

三、LinkedList源碼分析

  3.1 類的繼承關係 

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

  說明:LinkedList的類繼承結構很有意思,我們着重要看是Deque接口,Deque接口表示是一個雙端隊列,那麼也意味着LinkedList是雙端隊列的一種實現,所以,基於雙端隊列的操作在LinkedList中全部有效。

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

  說明:內部類Node就是實際的結點,用於存放實際元素的地方。

 3.3 類的屬性  

複製代碼
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    // 實際元素個數
    transient int size = 0;
    // 頭結點
    transient Node<E> first;
    // 尾結點
    transient Node<E> last;
}    
複製代碼

  說明:LinkedList的屬性非常簡單,一個頭結點、一個尾結點、一個表示鏈表中實際元素個數的變量。注意,頭結點、尾結點都有transient關鍵字修飾,這也意味着在序列化時該域是不會序列化的。

 3.4 類的構造函數

  1. LinkedList()型構造函數 

public LinkedList() {
}

  2. LinkedList(Collection<? extends E>)型構造函數  

複製代碼
    public LinkedList(Collection<? extends E> c) {
        // 調用無參構造函數
        this();
        // 添加集合中所有的元素
        addAll(c);
    }
複製代碼

  說明:會調用無參構造函數,並且會把集合中所有的元素添加到LinkedList中。

 3.5 核心函數分析

  1. add函數 

    public boolean add(E e) {
        // 添加到末尾
        linkLast(e);
        return true;
    }

  說明:add函數用於向LinkedList中添加一個元素,並且添加到鏈表尾部。具體添加到尾部的邏輯是由linkLast函數完成的。

複製代碼
    void linkLast(E e) {
        // 保存尾結點,l爲final類型,不可更改
        final Node<E> l = last;
        // 新生成結點的前驅爲l,後繼爲null
        final Node<E> newNode = new Node<>(l, e, null);
        // 重新賦值尾結點
        last = newNode;    
        if (l == null) // 尾結點爲空
            first = newNode; // 賦值頭結點
        else // 尾結點不爲空
            l.next = newNode; // 尾結點的後繼爲新生成的結點
        // 大小加1    
        size++;
        // 結構性修改加1
        modCount++;
    }
複製代碼

  說明:對於添加一個元素至鏈表中會調用add方法 -> linkLast方法。

  對於添加元素的情況我們使用如下示例進行說明

  示例一代碼如下(只展示了核心代碼) 

List<Integer> lists = new LinkedList<Integer>();
lists.add(5);
lists.add(6);

  說明:首先調用無參構造函數,之後添加元素5,之後再添加元素6。具體的示意圖如下:

  說明:上圖的表明了在執行每一條語句後,鏈表對應的狀態。

2. addAll函數

  addAll有兩個重載函數,addAll(Collection<? extends E>)型和addAll(int, Collection<? extends E>)型,我們平時習慣調用的addAll(Collection<? extends E>)型會轉化爲addAll(int, Collection<? extends E>)型,所以我們着重分析此函數即可。 

複製代碼
// 添加一個集合
    public boolean addAll(int index, Collection<? extends E> c) {
        // 檢查插入的的位置是否合法
        checkPositionIndex(index);
        // 將集合轉化爲數組
        Object[] a = c.toArray();
        // 保存集合大小
        int numNew = a.length;
        if (numNew == 0) // 集合爲空,直接返回
            return false;

        Node<E> pred, succ; // 前驅,後繼
        if (index == size) { // 如果插入位置爲鏈表末尾,則後繼爲null,前驅爲尾結點
            succ = null;
            pred = last;
        } else { // 插入位置爲其他某個位置
            succ = node(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) // 表示在第一個元素之前插入(索引爲0的結點)
                first = newNode;
            else
                pred.next = newNode;
            pred = newNode;
        }

        if (succ == null) { // 表示在最後一個元素之後插入
            last = pred;
        } else {
            pred.next = succ;
            succ.prev = pred;
        }
        // 修改實際元素個數
        size += numNew;
        // 結構性修改加1
        modCount++;
        return true;
    }
複製代碼

  說明:參數中的index表示在索引下標爲index的結點(實際上是第index + 1個結點)的前面插入。在addAll函數中,addAll函數中還會調用到node函數,get函數也會調用到node函數,此函數是根據索引下標找到該結點並返回,具體代碼如下

複製代碼
    Node<E> node(int 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; // 返回該結點
        }
    }
複製代碼

  說明:在根據索引查找結點時,會有一個小優化,結點在前半段則從頭開始遍歷,在後半段則從尾開始遍歷,這樣就保證了只需要遍歷最多一半結點就可以找到指定索引的結點。

  下面通過示例來更深入瞭解調用addAll函數後的鏈表狀態。  

List<Integer> lists = new LinkedList<Integer>();
lists.add(5);
lists.addAll(0, Arrays.asList(2, 3, 4, 5));

  上述代碼內部的鏈表結構如下:

  3. unlink函數

  在調用remove移除結點時,會調用到unlink函數,unlink函數具體如下:  

複製代碼
    E unlink(Node<E> x) {
        // 保存結點的元素
        final E element = x.item;
        // 保存x的後繼
        final Node<E> next = x.next;
        // 保存x的前驅
        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--; 
        // 結構性修改加1
        modCount++;
        // 返回結點的舊元素
        return element;
    }
複製代碼

  說明:將指定的結點從鏈表中斷開,不再累贅。

四、針對LinkedList的思考

  1. 對addAll函數的思考

  在addAll函數中,傳入一個集合參數和插入位置,然後將集合轉化爲數組,然後再遍歷數組,挨個添加數組的元素,但是問題來了,爲什麼要先轉化爲數組再進行遍歷,而不是直接遍歷集合呢?從效果上兩者是完全等價的,都可以達到遍歷的效果。關於爲什麼要轉化爲數組的問題,我的思考如下:1. 如果直接遍歷集合的話,那麼在遍歷過程中需要插入元素,在堆上分配內存空間,修改指針域,這個過程中就會一直佔用着這個集合,考慮正確同步的話,其他線程只能一直等待。2. 如果轉化爲數組,只需要遍歷集合,而遍歷集合過程中不需要額外的操作,所以佔用的時間相對是較短的,這樣就利於其他線程儘快的使用這個集合。說白了,就是有利於提高多線程訪問該集合的效率,儘可能短時間的阻塞。

五、總結

  分析完了LinkedList源碼,其實很簡單,值得注意的是LinkedList可以作爲雙端隊列使用,這也是隊列結構在Java中一種實現,當需要使用隊列結構時,可以考慮LinkedList。


List概括

        先來回顧一下List在Collection中的的框架圖:


    從圖中我們可以看出:

        1. List是一個接口,它繼承與Collection接口,代表有序的隊列。

        2. AbstractList是一個抽象類,它繼承與AbstractCollection。AbstractList實現了List接口中除了size()、get(int location)之外的方法。

        3. AbstractSequentialList是一個抽象類,它繼承與AbstrctList。AbstractSequentialList實現了“鏈表中,根據index索引值操作鏈表的全部方法”。

        4. ArrayList、LinkedList、Vector和Stack是List的四個實現類,其中Vector是基於JDK1.0,雖然實現了同步,但是效率低,已經不用了,Stack繼承與Vector,所以不再贅述。

        5. LinkedList是個雙向鏈表,它同樣可以被當作棧、隊列或雙端隊列來使用。

ArrayList和LinkedList區別

    我們知道,通常情況下,ArrayList和LinkedList的區別有以下幾點:

        1. ArrayList是實現了基於動態數組的數據結構,而LinkedList是基於鏈表的數據結構;

        2. 對於隨機訪問get和set,ArrayList要優於LinkedList,因爲LinkedList要移動指針;

       3. 對於添加和刪除操作add和remove,一般大家都會說LinkedList要比ArrayList快,因爲ArrayList要移動數據。但是實際情況並非這樣,對於添加或刪除,LinkedList和ArrayList並不能明確說明誰快誰慢,下面會詳細分析。

       從源碼可以看出,ArrayList想要get(int index)元素時,直接返回index位置上的元素,而LinkedList需要通過for循環進行查找,雖然LinkedList已經在查找方法上做了優化,比如index < size / 2,則從左邊開始查找,反之從右邊開始查找,但是還是比ArrayList要慢。這點是毋庸置疑的。
        ArrayList想要在指定位置插入或刪除元素時,主要耗時的是System.arraycopy動作,會移動index後面所有的元素;LinkedList主耗時的是要先通過for循環找到index,然後直接插入或刪除。這就導致了兩者並非一定誰快誰慢

       所以當插入的數據量很小時,兩者區別不太大,當插入的數據量大時,大約在容量的1/10之前,LinkedList會優於ArrayList,在其後就劣與ArrayList,且越靠近後面越差。所以個人覺得,一般首選用ArrayList,由於LinkedList可以實現棧、隊列以及雙端隊列等數據結構,所以當特定需要時候,使用LinkedList,當然咯,數據量小的時候,兩者差不多,視具體情況去選擇使用;當數據量大的時候,如果只需要在靠前的部分插入或刪除數據,那也可以選用LinkedList,反之選擇ArrayList反而效率更高。







發佈了27 篇原創文章 · 獲贊 48 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章