jdk集合源碼之LinkedList

LinkedList不同於ArrayList,它底層使用的是雙向循環鏈表實現的。繼承自AbstractSequentialList<E>不支持隨機訪問,同時一個LinkedList也是一個雙端隊列,支持從兩端的增刪,雙端隊列在某些多線程併發的場景下有很大的作用,比如兩個線程同時從頭和尾取數據,不需要加鎖。

要理解LinkedList的實現原理,先要認識下它的基礎存儲結構Entry

private transient Entry<E> header = new Entry<E>(null, null, null);
Entry(E element, Entry<E> next, Entry<E> previous) {
	    this.element = element;
	    this.next = next;
	    this.previous = previous;
	}
接收三個參數,自己節點的值,後繼,前驅。

上圖是一個Entry結構,也是整個LinkedList的頭節點,並且它的前後指針都是自指向的。當我們創建一個默認的LinkedList的時候,頭結點會自動創建出來。

/**
     * Constructs an empty list.
     */
    public LinkedList() {
        header.next = header.previous = header;
    }

看完構造函數在看看新增操作,LinkedList的add方法默認從末尾插入,add方法調用了內部的addBefore():

private Entry<E> addBefore(E e, Entry<E> entry) {
	Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
	newEntry.previous.next = newEntry;
	newEntry.next.previous = newEntry;
	size++;
	modCount++;
	return newEntry;
    }

此處第二個參數傳入的是header頭結點,假設我們調用LinkedList.add("a"),通過畫圖來看下整個過程:

初始只有一個header節點,該節點不存儲任何數據。



addBefore首先構造了一個新的entry節點,該節點的後繼指向header,前驅指向header的前驅,這裏也就是header本身,如下圖:


接着改變頭節點前後指針的指向:


對於雙向鏈表記住一個規則即可,如果a的前驅指向b,則b的後繼需要指向a。當我們構造a的時候,a的前驅指向header的前驅(header自己),所以header.next=a(這裏的header又是a.pre)-》a.pre.next=a,a的後繼指向header,所以header.pre=a(這裏的header又是a.next)-》a.next.pre=a,

如果再加節點b則變成下圖(畫圖工具還不太熟練,大概能看出來意思):


上面的新增都是在末尾新增,再來看下任意位置的新增操作:

public void add(int index, E element) {
        addBefore(element, (index==size ? header : entry(index)));
    }

初始有兩個元素,a和b。注意頭結點(值爲null)是一個虛擬節點,對我們來說不可見,因此這個雙向鏈表的size是2。


假設要插入的位置爲1元素爲c,entry(1)返回的是b這個節點,進入addBefore方法,需要把c包裝成一個entry,c節點的後繼指向p,前驅指向b的前驅即a,新增c後的結構如下圖:


構造完新節點的指向,然後需要改變b節點的前後指針,c前驅的後繼節點指向c,c後繼節點的前驅指向c。如下圖:


把上面的指向畫直就更顯而易見了,完成了任意位置的插入操作。再來看下更復雜的任意位置的批量插入,方法如下:

public boolean addAll(int index, Collection<? extends E> c) {
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Object[] a = c.toArray();
        int numNew = a.length;
        if (numNew==0)
            return false;
	modCount++;

        Entry<E> successor = (index==size ? header : entry(index));
        Entry<E> predecessor = successor.previous;
	for (int i=0; i<numNew; i++) {
            Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
            predecessor.next = e;
            predecessor = e;
        }
        successor.previous = predecessor;

        size += numNew;
        return true;
    }

假設當前的鏈表狀態如圖:


需要插入兩個新的元素c和d,插入的位置爲1,即b前面,首先獲取後繼此處爲b,獲取前驅a,這裏的前驅後繼可以想象成是要插入集合的前驅和後繼。接下來的循環操作是重點,仍然構造新節點,構造第一個節點c後的鏈表結構,和前面沒有區別:


整個操作我們遵循的是新增節點前驅指向誰,誰的後繼就得指向新增節點,所以改變a節點的後繼指向c,因爲後續還有節點要插入到c後面,所以c的後繼先不動,而是c變成當前的前驅節點,第一層循環後變成下圖:


第二層循環後:



最後,後繼的前驅指向當前的predecessor,把線弄直就是下面的結構圖:


新增的過程梳理完,刪除的過程就顯而易見了。任意位置的刪除操作,假設要刪除d,代碼如下:

private E remove(Entry<E> e) {
	if (e == header)
	    throw new NoSuchElementException();

        E result = e.element;
	e.previous.next = e.next;
	e.next.previous = e.previous;
        e.next = e.previous = null;
        e.element = null;
	size--;
	modCount++;
        return result;
    }

d的前驅的next指針不再指向d,直接指向d的下一個節點,即d.next;d的後繼的pre指針不再指向d,而是指向d的上一個節點,繼d.pre,將d的前驅後繼指向null,方便gc垃圾回收,如下圖:


新增和刪除看完後,後面的查找等操作就簡單不少了,查找主要通過下面的一個方法完成:

private Entry<E> entry(int index) {
        if (index < 0 || index >= size)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+size);
        Entry<E> e = header;
        if (index < (size >> 1)) {
            for (int i = 0; i <= index; i++)
                e = e.next;
        } else {
            for (int i = size; i > index; i--)
                e = e.previous;
        }
        return e;
    }

結合雙向鏈表的特點,這裏考慮到了性能,先判斷要查找的位置離最後一個節點近還是離頭結點近,分別做不同的操作。其他的一些方法沒有太大的難度就不逐一去分析了。下面重點看一下迭代器的使用。這裏的迭代器有兩種,第一種只能從前往後迭代,第二種迭代器可以從後往前實現雙向迭代。直接來看第二種迭代器ListIterator的實現,因爲第一種實現其實就是第二種實現的閹割版,底層調用的都是一段代碼,只是有些方法不可見了(向上轉型)。LinkedList實現了自己的雙向迭代器,並沒有使用父類提供的功能。

ListItr(int index) {
	    if (index < 0 || index > size)
		throw new IndexOutOfBoundsException("Index: "+index+
						    ", Size: "+size);
	    if (index < (size >> 1)) {
		next = header.next;
		for (nextIndex=0; nextIndex<index; nextIndex++)
		    next = next.next;
	    } else {
		next = header;
		for (nextIndex=size; nextIndex>index; nextIndex--)
		    next = next.previous;
	    }
	}

主要的部分就是構造函數關於初始狀態的設置,默認返回的是從頭結點開始的雙向迭代器,也可以指定具體的位置。整個迭代的過程主要使用nextIndex成員函數來作標記,它的位置時刻指向的當前節點的下一個節點的下標。


如果nextIndex大小等於size,則表明當前已經遍歷到了最後一個節點。如果nextIndex等於0則表明已經是第一個節點了。比如初始的情況,調用next返回的是a節點,nextIndex變成1,。當第四次調用next的時候返回的是最後一個元素b,此時的nextIndex爲4。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章