更新時間:2020-10-13 來源:黑馬程序員 瀏覽量:
LinkedList 集合底層是一個雙向鏈表結(jié)構(gòu),具有增刪快,查詢慢的忒點,內(nèi)部包含大量操作首尾元素的方法。適用于集合元素先入先出和先入后出的場景,在隊列源碼中被頻繁使用。
LinkedList 底層數(shù)據(jù)結(jié)構(gòu)是一個雙向鏈表,整體結(jié)構(gòu)如下圖所示:
上圖代表了一個雙向鏈表結(jié)構(gòu),可以通過前面的節(jié)點找到后面的節(jié)點,也可以通過后面的節(jié)點找到前面的節(jié)點
相關(guān)概念:
鏈表中的元素被稱為Node, Node被定義成私有靜態(tài)內(nèi)部類,內(nèi)容如下 :
private static class Node<E> {
E item;// 節(jié)點中存儲的數(shù)據(jù)
Node<E> next; // 下一個節(jié)點的地址
Node<E> prev; // 前一個節(jié)點的地址
// 構(gòu)造方法初始化參數(shù)順序分別是:前一個節(jié)點的地址值、當前節(jié)點中存儲的數(shù)據(jù)、后一個節(jié)點的地址值
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
如果想在LinkedList集合中添加節(jié)點,我們把新加入的節(jié)點添加到鏈表頭部,也可以把新加入的節(jié)點添加添加到鏈表尾部,add 方法默認是從尾部開始添加,addFirst 方法是從頭部開始添加,下面分別來看下兩種不同的添加方式:
從尾部添加(add)
// 從尾部開始添加節(jié)點
void linkLast(E e) {
// 把尾節(jié)點數(shù)據(jù)暫存
final Node<E> l = last;
// 新建新的節(jié)點,初始化入?yún)⒑x:
// l 是新節(jié)點的前一個節(jié)點,當前值是尾節(jié)點值
// e 表示當前新增節(jié)點,當前新增節(jié)點后一個節(jié)點是 null
final Node<E> newNode = new Node<>(l, e, null);
// 新建節(jié)點添加到尾部
last = newNode;
//如果鏈表為空(l 是尾節(jié)點,尾節(jié)點為空,鏈表即空),頭部和尾部是同一個節(jié)點,都是新建的節(jié)點
if (l == null)
first = newNode;
//否則把前尾節(jié)點的下一個節(jié)點,指向當前尾節(jié)點。
else
l.next = newNode;
size++;//集合元素數(shù)量增加1
modCount++;//實際修改次數(shù)增加1
}
從源碼上來看,尾部添加節(jié)點比較簡單.
從頭部添加(addFirst)
// 從頭部添加
private void linkFirst(E e) {
// 頭節(jié)點賦值給臨時變量
final Node<E> f = first;
// 新建節(jié)點,前一個節(jié)點指向null,e 是新建節(jié)點,f 是新建節(jié)點的下一個節(jié)點,目前值是頭節(jié)點的值
final Node<E> newNode = new Node<>(null, e, f);
// 新建節(jié)點成為頭節(jié)點
first = newNode;
// 頭節(jié)點為空,就是鏈表為空,頭尾節(jié)點是一個節(jié)點
if (f == null)
last = newNode;
//上一個頭節(jié)點的前一個節(jié)點指向當前節(jié)點
else
f.prev = newNode;
size++;
modCount++;
}
頭部添加節(jié)點和尾部添加節(jié)點非常類似,只是前者是移動頭節(jié)點的 prev 指向,后者是移動尾節(jié)點的 next 指向。
節(jié)點刪除的方式和添加類似,我們可以選擇從頭部刪除,也可以選擇從尾部刪除,刪除操作會把節(jié)點的值,前后指向節(jié)點都置為 null,幫助 GC 進行回收。
從頭部刪除
//從頭刪除節(jié)點 f 是鏈表頭節(jié)點
private E unlinkFirst(Node<E> f) {
// 拿出頭節(jié)點的值,作為方法的返回值
final E element = f.item;
// 拿出頭節(jié)點的下一個節(jié)點
final Node<E> next = f.next;
//幫助 GC 回收頭節(jié)點
f.item = null;
f.next = null;
// 頭節(jié)點的下一個節(jié)點成為頭節(jié)點
first = next;
//如果 next 為空,表明鏈表為空
if (next == null)
last = null;
//鏈表不為空,頭節(jié)點的前一個節(jié)點指向 null
else
next.prev = null;
//修改鏈表大小和版本
size--;
modCount++;
return element;
}
從尾部刪除節(jié)點的代碼也是類似的,這里就不再詳細解釋了。
從源碼中我們可以了解到,鏈表結(jié)構(gòu)的節(jié)點新增、刪除都非常簡單,僅僅把前后節(jié)點的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。
在鏈表查詢某一個節(jié)點是比較慢的,因為需要挨個循環(huán)查找才行,我們看看 LinkedList 的源碼是如何尋找節(jié)點的:
// 根據(jù)鏈表索引位置查詢節(jié)點
Node<E> node(int index) {
// 如果 index 處于隊列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
if (index < (size >> 1)) {
Node<E> x = first;
// 直到 for 循環(huán)到 index 的前一個 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果 index 處于隊列的后半部分,從尾開始找
Node<E> x = last;
// 直到 for 循環(huán)到 index 的后一個 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
從源碼中我們可以發(fā)現(xiàn),LinkedList 并沒有采用從頭循環(huán)到尾的做法,而是采取了簡單二分法,首先看看 index 是在鏈表的前半部分,還是后半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使循環(huán)的次數(shù)至少降低了一半,提高了查找的性能,這種思想值得我們借鑒。
因為 LinkedList 要實現(xiàn)雙向的迭代訪問,所以我們使用 Iterator 接口肯定不行了,因為 Iterator 只支持從頭到尾的訪問。Java 新增了一個迭代接口,叫做:ListIterator,這個接口提供了向前和向后的迭代方法,如下所示:
迭代順序 | 方法 |
---|---|
從尾到頭迭代方法 | hasPrevious、previous、previousIndex |
從頭到尾迭代方法 | hasNext、next、nextIndex |
LinkedList 實現(xiàn)了 ListIterator 接口,如下圖所示:
// 雙向迭代器
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;//上一次執(zhí)行 next() 或者 previos() 方法時的節(jié)點位置
private Node<E> next;//下一個節(jié)點
private int nextIndex;//下一個節(jié)點的位置
//expectedModCount:期望版本號;modCount:目前最新版本號
private int expectedModCount = modCount;
…………
}
我們先來看下從頭到尾方向的迭代:
// 判斷還有沒有下一個元素
public boolean hasNext() {
return nextIndex < size;// 下一個節(jié)點的索引小于鏈表的大小,就有
}
// 取下一個元素
public E next() {
//檢查期望版本號有無發(fā)生變化
checkForComodification();
if (!hasNext())//再次檢查
throw new NoSuchElementException();
// next 是當前節(jié)點,在上一次執(zhí)行 next() 方法時被賦值的。
// 第一次執(zhí)行時,是在初始化迭代器的時候,next 被賦值的
lastReturned = next;
// next 是下一個節(jié)點了,為下次迭代做準備
next = next.next;
nextIndex++;
return lastReturned.item;
}
上述源碼的思路就是直接取當前節(jié)點的下一個節(jié)點,而從尾到頭迭代稍微復雜一點,如下:
// 如果上次節(jié)點索引位置大于 0,就還有節(jié)點可以迭代
public boolean hasPrevious() {
return nextIndex > 0;
}
// 取前一個節(jié)點
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 為空場景:1:說明是第一次迭代,取尾節(jié)點(last);2:上一次操作把尾節(jié)點刪除掉了
// next 不為空場景:說明已經(jīng)發(fā)生過迭代了,直接取前一個節(jié)點即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置變化
nextIndex--;
return lastReturned.item;
}
這里復雜點體現(xiàn)在需要判斷 next 不為空和為空的場景,代碼注釋中有詳細的描述。
迭代器刪除
LinkedList 在刪除元素時,也推薦通過迭代器進行刪除,刪除過程如下:
public void remove() {
checkForComodification();
// lastReturned 是本次迭代需要刪除的值,分以下空和非空兩種情況:
// lastReturned 為空,說明調(diào)用者沒有主動執(zhí)行過 next() 或者 previos(),直接報錯
// lastReturned 不為空,是在上次執(zhí)行 next() 或者 previos()方法時賦的值
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//刪除當前節(jié)點
unlink(lastReturned);
// next == lastReturned 的場景分析:從尾到頭遞歸順序,并且是第一次迭代,并且要刪除最后一個元素的情況下
// 這種情況下,previous() 方法里面設(shè)置了 lastReturned = next = last,所以 next 和 lastReturned會相等
if (next == lastReturned)
// 這時候 lastReturned 是尾節(jié)點,lastNext 是 null,所以 next 也是 null,這樣在 previous() 執(zhí)行時,發(fā)現(xiàn) next 是 null,就會把尾節(jié)點賦值給 next
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
LinkedList 適用于要求有順序、并且會按照順序進行迭代的場景,主要是依賴于底層的鏈表結(jié)構(gòu),在面試中的頻率還是蠻高的,相信理清楚上面的源碼后,應對面試應該沒有問題。