面試老被問LinkedList原始碼?看看阿里技術官是怎麼深度剖析的吧!
前言
LinkedList底層是基於雙向連結串列,連結串列在記憶體中不是連續的,而是通過引用來關聯所有的元素,所以連結串列的優點在於新增和刪除元素比較快,因為只是移動指標,並且不需要判斷是否需要擴容,缺點是查詢和遍歷效率比較低。下面會給大家詳細的剖析一下底層原始碼!
結構
LinkedList 繼承關係,核心成員變數,主要建構函式:
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { // Node,雙向連結串列 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; } } //------------------------成員變數------------------------------------- transient int size = 0; // 記錄頭結點,它的前一個結點=null transient Node<E> first; // 記錄尾結點,它的後一個結點=null // 當 first = last = null時表示連結串列為空 // 當 first = last != null時表示只有一個節點 transient Node<E> last; //--------------------------構造方法------------------------------------- public LinkedList() { } public LinkedList(Collection<? extends E> c) { this(); addAll(c); } // ........ }
方法解析&api
追加
追加節點時,我們可以選擇追加到連結串列頭部,還是追加到連結串列尾部,add 方法預設是從尾部開始追加,addFirst 方法是從頭部開始追加,我們分別來看下兩種不同的追加方式:
-
add()
public boolean add(E e) {
linkLast(e);
return true;
}
--
linkLast()
/** * 尾插 * newNode.pre = last * last.next = newNode 注:考慮last=null情況(連結串列為空,這時僅更新頭結點即可) * last = newNode */ void linkLast(E e) { // 把尾節點資料暫存,為last.next做準備,其實改變一下順序就可以不要這個l了 final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); // 1 last = newNode; // 2 // 空連結串列,l=null,l.next報空指標 if (l == null) first = newNode; else l.next = newNode; // 3 // size和版本更改 size++; modCount++; }
-
addFirst()
public void addFirst(E e) {
linkFirst(e);
}
--
linkFirst()
/** * 頭插 * newNode.next = first; * first.prev = newNode; 注:考慮first=null(連結串列為空,只用更新last即可) * first = newNode; */ private void linkFirst(E e) { // 頭節點賦值給臨時變數 final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); // 1 first = newNode; // 2 // 連結串列為空,f=null, f.prev報空指標 if (f == null) last = newNode; else f.prev = newNode; // 3 // 更新size和版本號 size++; modCount++; }
刪除
節點刪除的方式和追加類似,我們可以刪除指定元素,或者從頭部(尾部)刪除,刪除操作會把節點的值,前後指向節點都置為 null,幫助 GC 進行回收
-
remove()
/**
*刪除指定元素;找到要刪除的節點
*注:只有連結串列有這個節點且成功刪除才返回true
*/
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
// null用 == 判斷
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
// 呼叫equals判斷,若傳入的類無equals需要重寫
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false; // 連結串列無要刪除元素,或連結串列為空
}
注:remove還可以根據索引刪除
public E remove(int index) {
checkElementIndex(index); // 連結串列為空,丟擲異常
return unlink(node(index));
}
--
unlink()
/**
* 執行刪除
* x.prev.next = x.next 注:考慮x.prev=null(x是first,直接更新first)
* x.next.prev = x.prev.prev 注:考慮x.next=null(x是last,直接更新last)
*/
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;
// 如果prev=null,則當前節點為頭結點
if (prev == null) {
// 直接將頭結點賦成next
first = next;
} else {
prev.next = next; // 1
x.prev = null; // 幫助 GC 回收該節點
}
// 如果next=null,則當前節點為尾結點
if (next == null) {
last = prev;
} else {
next.prev = prev; // 2
x.next = null; // 幫助 GC 回收該節點
}
x.item = null; // 幫助 GC 回收該節點
// 修改size及版本
size--;
modCount++;
return element;
}
-
remove()
/**
*刪除頭節點,佇列為空時丟擲異常
*/
public E remove() {
return removeFirst();
}
-
removeFirst()
/**
*刪除頭節點
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
--
unLinkFirst()
/**
* 執行刪除頭節點
* first.next.pre = null; 注:考慮first=null(連結串列為空), first.next=null(尾結點,即連結串列僅一個節點)
* first = first.next;
*/
private E unlinkFirst(Node<E> f) {
final E element = f.item; // 拿出頭節點的值,作為方法的返回值
final Node<E> next = f.next; // 拿出頭節點的下一個節點
//幫助 GC 回收頭節點
f.item = null;
f.next = null;
first = next; // 1
// next為空表示連結串列只有一個節點
if (next == null)
last = null;
else
next.prev = null; // 2
//修改連結串列大小和版本
size--;
modCount++;
return element;
}
從原始碼中我們可以瞭解到,連結串列結構的節點新增、刪除都非常簡單,僅僅把前後節點的指向修改下就好了,所以 LinkedList 新增和刪除速度很快。
查詢
連結串列查詢某一個節點是比較慢的,需要挨個迴圈查詢才行,我們看看 LinkedList 的原始碼是如何尋找節點的
-
get()
/**
*根據索引進行查詢
*/
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
--
node()
Node<E> node(int index) {
// 如果 index 處於佇列的前半部分,從頭開始找,size >> 1 是 size 除以 2 的意思。
if (index < (size >> 1)) {
// 取頭節點
Node<E> x = first;
// 直到 for 迴圈到 index 的前一個 node 停止
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {// 如果 index 處於佇列的後半部分,從尾開始找
// 取尾結點
Node<E> x = last;
// 直到 for 迴圈到 index 的後一個 node 停止
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
從原始碼中我們可以發現,LinkedList 並沒有採用從頭迴圈到尾的做法,而是採取了簡單二分法,首先看看 index 是在連結串列的前半部分,還是後半部分。如果是前半部分,就從頭開始尋找,反之亦然。通過這種方式,使迴圈的次數至少降低了一半,提高了查詢的效能,這種思想值得我們借鑑
迭代器
因為 LinkedList 要實現雙向的迭代訪問,所以使用 Iterator 介面肯定不行了,因為 Iterator 只支援從頭到尾的訪問。Java 新增了一個迭代介面,叫做:ListIterator,這個介面提供了向前和向後的迭代方法,如下所示:
迭代順序 | 方法 |
---|---|
從尾到頭迭代方法 | hasPrevious、previous、previousIndex |
從頭到尾迭代方法 | hasNext、next、nextIndex |
-
listIterator()
/**
*從指定節點開始迭代,可前可後
*/
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
/**
*ListItr,雙向迭代器
*/
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;//上一次執行 next() 或者 previos() 方法時的節點位置
private Node<E> next;//下一個節點
private int nextIndex;//下一個節點的位置
//expectedModCount:期望版本號;modCount:目前最新版本號
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
}
--
hasNext()
從前向後迭代
// 判斷還有沒有下一個元素,還是通過index和size控制
public boolean hasNext() {
return nextIndex < size;// 下一個節點的索引小於連結串列的大小,就有
}
---
next()
// 取下一個元素,並後移
public E next() {
//檢查期望版本號有無發生變化
checkForComodification();
if (!hasNext())//再次檢查
throw new NoSuchElementException();
// next 是當前節點,在上一次執行 next() 方法時被賦值的。
// 第一次執行時,是在初始化迭代器的時候,next 被賦值的
lastReturned = next;
// next 是下一個節點了,為下次迭代做準備
next = next.next;
nextIndex++;
return lastReturned.item;
}
--
hasPrevious()
從後向前迭代
// 如果上次節點索引位置大於 0,就還有節點可以迭代
public boolean hasPrevious() {
return nextIndex > 0;
}
---
previous()
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 為空場景:1:說明是第一次迭代,取尾節點(last);2:上一次操作把尾節點刪除掉了
// next 不為空場景:說明已經發生過迭代了,直接取前一個節點即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置變化
nextIndex--;
return lastReturned.item;
}
----
remove()
/**
*迭代時,刪除當前元素
*/
public void remove() {
checkForComodification();
// lastReturned 是本次迭代需要刪除的值,分以下空和非空兩種情況:
// lastReturned 為空,說明呼叫者沒有主動執行過 next() 或者 previos(),直接報錯
// lastReturned 不為空,是在上次執行 next() 或者 previos()方法時賦的值
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//刪除當前節點
unlink(lastReturned);
// next == lastReturned 的場景分析:從尾到頭遞迴順序,並且是第一次迭代,並且要刪除最後一個元素的情況
// 這種情況下,previous()方法裡面設定了 lastReturned=next=last,所以 next 和l astReturned 會相等
if (next == lastReturned)
// 這時候 lastReturned 是尾節點,lastNext 是 null,所以 next 也是 null,這樣在 previous() 執行 // 時,發現 next 是 null,就會把尾節點賦值給 next
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
Queue的實現
LinkedList 實現了 Queue 介面,在新增、刪除、查詢等方面增加了很多新的方法,這些方法在平時特別容易混淆,在連結串列為空的情況下,返回值也不太一樣,下面列一個表格,方便大家記錄:
PS:Queue 介面註釋建議 add 方法操作失敗時丟擲異常,但 LinkedList 實現的 add 方法一直返回 true。
LinkedList 也實現了 Deque 介面,對新增、刪除和查詢都提供從頭開始,還是從尾開始兩種方向的方法,比如 remove 方法,Deque 提供了 removeFirst 和 removeLast 兩種方向的使用方式,但當連結串列為空時的表現都和 remove 方法一樣,都會丟擲異常。
最後
感謝你看到這裡,文章有什麼不足還請指正,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!