Java小白集合原始碼的學習系列:LinkedList
目錄
- LinkedList 原始碼學習
- LinkedList繼承體系
- LinkedList核心原始碼
- Deque相關操作
- 總結
LinkedList 原始碼學習
前文傳送門:Java小白集合原始碼的學習系列:ArrayList
本篇為集合原始碼學習系列的LinkedList
學習部分,如有敘述不當之處,還望評論區批評指正!
LinkedList繼承體系
LinkedList和ArrayList一樣,都實現了List介面,都代表著列表結構,都有著類似的add,remove,clear等操作。與ArrayList不同的是,LinkedList底層基於雙向連結串列,允許不連續地址的儲存,通過節點之間的相互引用建立聯絡,通過節點儲存資料。
LinkedList核心原始碼
既然是基於節點的,那麼我們來看看節點在LinkedList中是怎樣的存在:
//Node作為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; } }
我們發現,Node作為其內部類,擁有三個屬性,一個是用來指向前一節點的指標prev,一個是指向後一節點的指標next,還有儲存的元素值item。
我們來看看LinkedList的幾個基本屬性:
/*用transient關鍵字標記的成員變數不參與序列化過程*/ transient int size = 0;//記錄節點個數 /** * first是指向第一個節點的指標。永遠只有下面兩種情況: * 1、連結串列為空,此時first和last同時為空。 * 2、連結串列不為空,此時第一個節點不為空,第一個節點的prev指標指向空 */ transient Node<E> first; /** * last是指向最後一個節點的指標,同樣地,也只有兩種情況: * 1、連結串列為空,first和last同時為空 * 2、連結串列不為空,此時最後一個節點不為空,其next指向空 */ transient Node<E> last; //需要注意的是,當first和last指向同一節點時,表明連結串列中只有一個節點。
瞭解基本屬性之後,我們看看它的構造方法,由於不必在乎它儲存的位置,它的構造器也是相當簡單的:
//建立一個空連結串列
public LinkedList() {
}
//建立一個連結串列,包含指定傳入的所有元素,這些元素按照迭代順序排列
public LinkedList(Collection<? extends E> c) {
this();
//新增操作
addAll(c);
}
其中addAll(c)其實呼叫了addAll(size,c),由於這裡size=0,所以相當於從頭開始一一新增。至於addAll方法,我們暫時不提,當我們總結完普通的新增操作,也就自然明瞭這個全部新增的操作。
//把e作為連結串列的第一個元素
private void linkFirst(E e) {
//建立臨時節點指向first
final Node<E> f = first;
//建立儲存e的新節點,prev指向null,next指向臨時節點
final Node<E> newNode = new Node<>(null, e, f);
//這時newNode變成了第一個節點,將first指向它
first = newNode;
//對原來的first,也就是現在的臨時節點f進行判斷
if (f == null)
//原來的first為null,說明原來沒有節點,現在的newNode
//是唯一的節點,所以讓last也只想newNode
last = newNode;
else
//原來連結串列不為空,讓原來頭節點的prev指向newNode
f.prev = newNode;
//節點數量加一
size++;
//對列表進行改動,modCount計數加一
modCount++;
}
相應的,把元素作為連結串列的最後一個元素新增和第一個元素新增方法類似,就不贅述了。我們來看看我們一開始遇到的addAll操作,感覺有一點點麻煩的哦:
//在指定位置把另一個集合中的所有元素按照迭代順序新增進來,如果發生改變,返回true
public boolean addAll(int index, Collection<? extends E> c) {
//範圍判斷
checkPositionIndex(index);
//將集合轉換為陣列,果傳入集合為null,會出現空指標異常
Object[] a = c.toArray();
//傳入集合元素個數為0,沒有改變原集合,返回false
int numNew = a.length;
if (numNew == 0)
return false;
//建立兩個臨時節點,暫時表示新表的頭和尾
Node<E> pred, succ;
//相當於從原集合的尾部新增
if (index == size) {
//暫時讓succ置空
succ = null;
//讓pred指向原集合的最後一個節點
pred = last;
} else {
//如果從中間插入,則讓succ指向指定索引位置上的節點
succ = node(index);
//讓succ的prev指向pred
pred = succ.prev;
}
//增強for迴圈遍歷賦值
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
//建立儲存值尾e的新節點,前向指標指向pred,後向指標指向null
Node<E> newNode = new Node<>(pred, e, null);
//表明原連結串列為空,此時讓first指向新節點
if (pred == null);
first = newNode;
else
//原連結串列不為空,就讓臨時節點pred節點向後移動
pred.next = newNode;
//更新新表的頭節點為當前新建立的節點
pred = newNode;
}
//這種情況出現在原連結串列後面插入
if (succ == null) {
//此時pred就是最終連結串列的last
last = pred;
} else {
//在index處插入的情況
//由於succ是node(index)的臨時節點,pred因為遍歷也到了插入連結串列的最後一個節點
//讓最後位置的pred和succ建立聯絡
pred.next = succ;
succ.prev = pred;
}
//新長度為原長+增長
size += numNew;
modCount++;
return true;
}
- 注意:遍歷賦值的過程相當於從pred這個臨時節點開始,依次向後建立新節點,並將pred向後移動,直到新傳入集合的最後一個元素,這時再將pred和succ兩個建立聯絡,實現無縫連結。
再來看看,在連結串列中普通刪除元素的操作是怎麼樣的:
//取消一個非空節點x的連結,並返回它
E unlink(Node<E> x) {
//同樣的,在呼叫這個方法之前,需要確保x不為空
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//明確x與上一節點的聯絡,更新並刪除無用聯絡
//x為頭節點
if (prev == null) {
//讓first指向x.next的臨時節點next,宣佈從下一節點開始才是頭
first = next;
} else {
//x不是頭節點的情況
//讓x.prev的臨時節點prev的next指向x.next的臨時節點
prev.next = next;
//刪除x的前向引用,即讓x.prev置空
x.prev = null;
}
//明確x與下一節點的聯絡,更新並刪除無用聯絡
//x為尾節點
if (next == null) {
//讓last指向x.prev的臨時節點prev,宣佈上一節點是最後的尾
last = prev;
} else {
//x不是尾節點的情況
//讓x.next的臨時節點next的prev指向x.prev的臨時節點
next.prev = prev;
//刪除x的後向引用,讓x.next置空
x.next = null;
}
//讓x儲存元素置空,等待GC寵信
x.item = null;
size--;
modCount++;
return element;
}
總結來說,刪除操作無非就是,消除該節點與另外兩個節點的聯絡,並讓與它相鄰的兩個節點之間建立聯絡。如果考慮邊界條件的話,比如為頭節點和尾節點的情況,需要再另加分析。總之,它不需要向ArrayList一樣,拷貝陣列,而是改變節點間的地址引用。但是,刪除之前需要找到這個節點,我們還是需要遍歷滴,就像下面這樣:
//移除第一次出現的元素o,找到並移除返回true,否則false
public boolean remove(Object o) {
//傳入元素本身就為null
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) {
//刪除的元素不為null,比較值的大小
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
總結一下從前向後遍歷的過程:
- 建立一個臨時節點指向first。
- 向後遍歷,讓臨時節點指向它的下一位。
- 直到臨時節點指向last的下一位(即x==null)為止。
當然特殊情況特殊考慮,上面的remove方法目的是找到對應的元素,只需要在迴圈中加入相應的邏輯判斷即可。下面這個相當重要的輔助方法就是通過遍歷獲取指定位置上的節點:有了這個方法,我們就可以同過它的前後位置,推匯出其他不同的方法:
//獲得指定位置上的非空節點
Node<E> node(int index) {
//在呼叫這個方法之前會確保0<=inedx<size
//index和size>>1比較,如果index比size的一半小,從前向後遍歷
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
//退出迴圈的條件,i==indx,此時x為當前節點
return x;
} else {
//從後向前遍歷
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
與此同時還有indexOf和lastIndexOf方法也是通過上面總結的遍歷過程,加上計數條件,計算出指定元素第一次或者最後一次出現的索引,這裡以indexOf為例:
//返回元素第一次出現的位置,沒找到就返回-1
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;
}
其實就是我們上面講的遍歷操作嘛,大差不差。有了這個方法,我們還是可以很輕鬆地推匯出另外的contains方法。
public boolean contains(Object o) {
return indexOf(o) != -1;
}
然後還是那對基佬方法:get和set。
//獲取元素值
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//用新值替換舊值,返回舊值
public E set(int index, E element) {
checkElementIndex(index);
//獲取節點
Node<E> x = node(index);
//存取舊值
E oldVal = x.item;
//替換舊值
x.item = element;
//返回舊值
return oldVal;
}
接下來是我們的clear方法,移除所有的元素,將表置空。雖然寫法有所不同,但是基本思想是不變的:建立節點,並移動,刪除不要的,或者找到需要的,就行了。
public void clear() {
for (Node<E> x = first; x != null; ) {
//建立臨時節點指向當前節點的下一位
Node<E> next = x.next;
//下面就可以安心地把當前節點有關的全部清除
x.item = null;
x.next = null;
x.prev = null;
//x向後移動
x = next;
}
//回到最初的起點
first = last = null;
size = 0;
modCount++;
}
Deque相關操作
我們還知道,LinkedList還繼承了Deque介面,讓我們能夠操作佇列一樣操作它,下面是擷取不完全的一些方法:
我們從中挑選幾個分析一下,幾個具有迷惑性方法的差異,比如下面這四個:
public E element() {
return getFirst();
}
public E getFirst() {
final Node<E> f = first;
//如果頭節點為空,丟擲異常
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
- element:呼叫getFirst方法,如果頭節點為空,丟擲異常。
- getFirst:如果頭節點為空,丟擲異常。
- peek:頭節點為空,返回null。
- peekFirst:頭節點為空,返回null。
與之類似的還有:
- pollFirst和pollLast方法刪除頭和尾節點,如果為空,返回null。
- removeFirst和removeFirst如果為空,拋異常。
如果有興趣的話,可以研究一下,總之還是相對簡單的。
總結
而LinkedList底層基於雙向連結串列實現,不需要連續的記憶體儲存,通過節點之間相互引用地址形成聯絡。
對於無索引位置的插入來說,例如向後插入,時間複雜度近似為O(1),體現出增刪操作較快。但是如果要在指定的位置上插入,還是需要移動到當前指定索引位置,才可以進行操作,時間複雜度近似為O(n)。
Linkedlist不支援快速隨機訪問,查詢較慢。
執行緒不安全,同樣的,關於執行緒方面,以後學習時再進行總結。