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。