1. 程式人生 > >Java集合 List實現類 LinkedList (雙鏈表) 原始碼淺析

Java集合 List實現類 LinkedList (雙鏈表) 原始碼淺析

Java集合 List實現類 LinkedList 原始碼淺析

文章目錄

LinkedList原始碼實現基於 jdk 1.8

原始碼來源於 jdk 1.8

一 、簡述(來自JAVA api 註釋)

List 介面的連結列表實現。實現所有可選的列表操作,並且允許所有元素(包括 null)。除了實現 List 介面外,LinkedList 類還為在列表的開頭及結尾 getremoveinsert 元素提供了統一的命名方法。這些操作允許將連結列表用作堆疊、佇列或雙端佇列。

此類實現 Deque 介面,為 addpoll 提供先進先出佇列操作,以及其他堆疊和雙端佇列操作。

所有操作都是按照雙重連結列表的需要執行的。在列表中編索引的操作將從開頭或結尾遍歷列表(從靠近指定索引的一端)。

注意,此實現不是同步的。 如果多個執行緒同時訪問一個連結列表,而其中至少一個執行緒從結構上修改了該列表,則它必須 保持外部同步。(結構修改指新增或刪除一個或多個元素的任何操作;僅設定元素的值不是結構修改。)這一般通過對自然封裝該列表的物件進行同步操作來完成。如果不存在這樣的物件,則應該使用 Collections.synchronizedList 方法來“包裝”該列表。最好在建立時完成這一操作,以防止對列表進行意外的不同步訪問,如下所示:

  List list = Collections.synchronizedList(new LinkedList(...));

此類的 iteratorlistIterator 方法返回的迭代器是快速失敗 的:在迭代器建立之後,如果從結構上對列表進行修改,除非通過迭代器自身的 removeadd 方法,其他任何時間任何方式的修改,迭代器都將丟擲 ConcurrentModificationException。因此,面對併發的修改,迭代器很快就會完全失敗,而不冒將來不確定的時間任意發生不確定行為的風險。

注意,迭代器的快速失敗行為不能得到保證,一般來說,存在不同步的併發修改時,不可能作出任何硬性保證。快速失敗迭代器盡最大努力丟擲 ConcurrentModificationException。因此,編寫依賴於此異常的程式的方式是錯誤的,正確做法是:迭代器的快速失敗行為應該僅用於檢測程式錯誤。

此類是 Java Collections Framework 的成員。

類圖
在這裡插入圖片描述

此處只探討List 方法的實現, 對於 Queue 和 Deque 佇列的實現,稍後有專門的章節探討.

點選此處 Queue 和 Deque 實現

二、構造方法

LinkedList() 和 LinkedList(Collection<? extends E> c) ;

	// 構造一個空列表。
    public LinkedList() {
    }
    /**
    * 構造一個包含指定 collection 中的元素的列表,
    * 這些元素按其 collection 的迭代器返回的順序排列。
    */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

三、List方法

在分析原始碼前,先看看LinkedList 是如何儲存元素的?
LinkedList 裡有一個內部類, Node (節點) ;

型別 / 欄位 描述
E item 泛型, 存放著新增進去的元素
Node next 存放下一個節點的指標
Node prev 存放上一個節點的指標

LinkedList 裡有兩個屬性 first 和 last

first 指向第一個節點的指標
last 指向最後一個節點的指標

由下圖可以看出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;
        }
    }

1、add(E e)

原始碼分析: 宣告一個記錄尾節點的指標 l, l = last;新建一個節點 new Node<>(l, e, null), prev = last, next = null,
即前一個指標指向原來的尾節點, 下一個節點為null; 如果為節點l為null, 頭節點等於尾節點; 如果不為null,原來的尾節點的下一個節點的指標指向當前新鍵的節點 newNode.
時間複雜度為 O(1)

	// 將指定元素新增到此列表的結尾。
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    // 連結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 = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

2、add(int index, E element)

原始碼註釋: 將指定元素插入此列表中的指定位置。 將當前位置的元素(如果有)和任何後續元素向右移動(將其新增到索引中)。

原始碼解析: 新增元素前, 檢查索引是否越界, 如果越界跑錯異常;
如果: 索引等於連結串列長度, 則新增到連結串列的尾部;
如果: 索引小於連結串列長度,則先獲取當前索引的節點, 在把元素新增到這個節點的前面;

時序圖
在這裡插入圖片描述

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

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }
    // 檢查索引是否有效,否則丟擲 IndexOutOfBoundsException 異常
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    // 判斷引數是否是迭代器或新增操作的有效位置的索引。
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
    // 構造一個下標越界的異常資訊,(java.lang.IndexOutOfBoundsException: Index: 44, Size: 3)
    private String outOfBoundsMsg(int index) {
        return "Index: "+index+", Size: "+size;
    }
    // 返回指定元素索引處的(非null)節點。
    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;
        }
    }
    // 在非null節點succ之前插入元素e。
    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++;
    }

3. 獲取指定元素 get(int index)

原始碼註釋: 返回此列表中指定位置的元素。

原始碼分析: 先檢查元素索引, 然後呼叫 node() 獲取指定索引的元素;
獲取元素的演算法為遍歷連結串列, 時間複雜度為 O(n)

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    // 檢查元素索引, 如果索引超出範圍 (index < 0 || index >= size())
    // 則丟擲異常 
    private void checkElementIndex(int index) {
        if (!isElementIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    // 判斷引數是否是現有元素的索引。
    private boolean isElementIndex(int index) {
        return index >= 0 && index < size;
    }

4. 替換元素 set(int index, E element)

原始碼註釋: 用指定的元素替換此列表中指定位置的元素。

原始碼分析: 檢查索引, 呼叫 node() 方法獲取指定位置的節點, 然後替換節點的元素 x.item = element;

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

5.移除元素 remove(int index)

原始碼註釋: 刪除此列表中指定位置的元素。 將任何後續元素向左移位(從索引中減去一個元素)。 返回從列表中刪除的元素。

原始碼分析: 首先檢查元素索引, 然後呼叫 node 獲取指定索引的節點, 再呼叫 unlink 斷開這個節點;
unlink 會判斷這個節點是否為首節點first 或是尾節點 last

在這裡插入圖片描述

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
    // 斷開非空節點 x
    // 這個方法的操作大致分三段, 輸入需要斷開的節點 x,獲取 x節點的前一個指標/下一個節點指標/當前元素指標
    // 1.判斷前一個節點指標是否為null,如果為true, 即節點x 就是首節點 first
    // 2.判斷下一個節點指標是否為null, 如果為true, 即節點x 就是尾節點 last
    // 3.當前元素指標置空, size--
    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;
    }

6.移除指定元素 remove(Object o)

原始碼註釋: 從該列表中刪除指定元素的第一個匹配項(如果存在)。 如果此列表不包含該元素,則不會更改。 更正式地,刪除具有最低索引i的元素,使得 (o == null ? get(i)==null : o.equals(get(i))) (如果存在這樣的元素)。 如果此列表包含指定的元素,則返回true(或等效地,如果此列表因呼叫而更改)。

原始碼分析: 程式碼的邏輯 , 則迭代連結串列,使用引數 o與每個元素比較, 找到最早匹配到的元素, 最後呼叫 unlink() 方法斷開節點

    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

7.連結串列的元素數目 size()

原始碼註釋: 返回此列表中的元素數。

    public int size() {
        return size;
    }

8. 判斷是否為空 isEmpty()

原始碼註釋: 如果此collection不包含任何元素,則返回true。
此實現返回 size() == 0

    public boolean isEmpty() {
        return size() == 0;
    }

9.清空連結串列元素 clear()

原始碼註釋: 從此列表中刪除所有元素。 此呼叫返回後,列表將為空。

原始碼分析: 迭代連結串列把每個節點的指標置空, 此操作後 size = 0, 首節點和尾節點指標為null, modCount 計數器 +1

    public void clear() {
		 //清除節點之間的所有連結是“不必要的”,但是:
         //  - 如果丟棄的節點居住,則幫助生成GC
         //不止一代
         //  - 即使有可達的迭代器,也一定要釋放記憶體
        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++;
    }

10.查詢指定元素索引 indexOf(Object o)

原始碼註釋: 返回此列表中第一次出現的指定元素的索引,如果此列表不包含該元素,則返回-1。
更正式地,返回最低索引i,使得 (o==null ? get(i)==null : o.equals(get(i))) ,如果沒有這樣的索引則返回-1。

原始碼分析: 此處的邏輯與 remove(Object o) 方法的邏輯一樣, 只是最後一步的操作有差異.此處最後一步是返回匹配到的元素索引

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

11.判斷是否包含指定元素 contains(Object o)

原始碼註釋: 如果此列表包含指定的元素,則返回true。 更正式地,當且僅當此列表包含至少一個元素e時才返回true
(o == null ? e == null : o.equals(e))

原始碼分析: 這個方法是indexOf() 方法的擴充套件版本;

    public boolean contains(Object o) {
        return indexOf(o) != -1;
    }

12.是否包含指定集合的所有元素 containsAll(Collection<?> c)

原始碼註釋: 如果此collection包含指定collection中的所有元素,則返回true。
此實現迭代指定的集合,依次檢查迭代器返回的每個元素以檢視它是否包含在此集合中。 如果包含所有元素,則返回true,否則返回false。

原始碼分析: 首先迭代引數集合c, 使用集合c的每個元素作為contains()方法的引數, 搜尋元素.如果出現一次 false, 馬上返回, 即連結串列不包含這個集合c的所有元素;
這個時間複雜度可以理解為 O(n^2), 因為此處是兩個for迴圈巢狀

    public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }

13.刪除包含指定集合的元素 removeAll(Collection<?> c)

原始碼註釋: 刪除此集合的所有元素,這些元素也包含在指定的集合中(可選操作)。 此呼叫返回後,此集合將不包含與指定集合相同的元素。
此實現迭代此集合,依次檢查迭代器返回的每個元素,以檢視它是否包含在指定的集合中。 如果包含它,則使用迭代器的remove方法將其從此集合中刪除。
請注意,如果迭代器方法返回的迭代器未實現remove方法,並且此collection包含與指定collection相同的一個或多個元素,則此實現將丟擲UnsupportedOperationException

原始碼分析: 先判斷c是否非空, 再獲取當前連結串列的迭代器, 呼叫集合c 的 contains()方法搜尋迭代器返回的元素it.next;如果返回true就呼叫迭代器的 remove()方法移除這個元素;
由此可知, 假若獲取的迭代器不支援 remove方法,則丟擲異常 UnsupportedOperationException

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;
        Iterator<?> it = iterator();
        while (it.hasNext()) {
            if (c.contains(it.next())) {
                it.remove();
                modified = true;
            }
        }
        return modified;
    }

14.連結串列轉陣列 toArray()

原始碼註釋: 以適當的順序(從第一個元素到最後一個元素)返回包含此列表中所有元素的陣列。

返回的陣列將是“安全的”,因為此列表不會保留對它的引用。 (換句話說,此方法必須分配一個新陣列)。 因此呼叫者可以自由修改返回的陣列。

此方法充當基於陣列和基於集合的API之間的橋樑。

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

15.連結串列轉陣列, 返回執行時型別 toArray(T[] a)

原始碼註釋: 以適當的順序返回包含此列表中所有元素的陣列(從第一個元素到最後一個元素);返回陣列的執行時型別是指定陣列的執行時型別。如果列表適合指定的陣列,則返回其中。否則,將為新陣列分配指定陣列的執行時型別和此列表的大小。

如果列表適合指定的陣列,並且有空餘空間(即,陣列的元素多於列表),則緊跟在列表末尾的陣列中的元素將設定為null。 (僅當呼叫者知道列表不包含任何null元素時,這在確定列表長度時很有用。)

與toArray()方法一樣,此方法充當基於陣列的API和基於集合的API之間的橋樑。此外,該方法允許精確控制輸出陣列的執行時型別,並且在某些情況下可以用於節省分配成本。

假設x是已知僅包含字串的列表。以下程式碼可用於將列表轉儲到新分配的String陣列中:

   String [] y = x.toArray(new String [0]);

請注意,toArray(new Object [0]) 在功能上與toArray() 相同。

    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            a = (T[])java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size);
        int i = 0;
        Object[] result = a;
        for (Node<E> x = first; x != null; x = x.next)
            result[i++] = x.item;
        if (a.length > size)
            a[size] = null;
        return a;
    }

16.排序sort()

原始碼註釋: 根據指定的比較器引發的順序對此列表進行排序。

此列表中的所有元素必須使用指定的比較器進行相互比較(即,c.compare(e1,e2) 不得對列表中的任何元素e1和e2丟擲ClassCastException)。

如果指定的比較器為null,則此列表中的所有元素都必須實現Comparable介面,並且應使用元素的自然順序。

此列表必須是可修改的,但無需調整大小。

原始碼分析: 這個方法帶有 default 關鍵字, 是 List 接口裡的預設方法
原始碼邏輯, 輸入一個比較器Comparator; 先呼叫 toArray() 獲取一個新陣列 a, 再排序陣列a;
然後獲取當前連結串列的迭代器, 再遍歷a,把a 的每個值重新設定到鏈中, 因為 set()方法不會修改連結串列的結構, 所以不會丟擲 ConcurrentModificationException
這個排序的效率關鍵在於 Arrays.sort() 方法的時間複雜度

    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

17. hashCode() 與 equals(Object o)

hashCode() 原始碼註釋: 返回此列表的雜湊碼值。

此實現完全使用用於在List.hashCode方法的文件中定義列表雜湊函式的程式碼。

    public int