LinkedList里面涉及到的一些操作,非常細致,以避免出現的空指針,理解后對于其優點與確定會有一個更加整體的認識吧。
繼承關系圖(對比ArrayList)
元素的存儲結構 在LinkedList中,每一個元素都是Node存儲,Node擁有一個存儲值的item與一個前驅prev和一個后繼next,如下:
// 典型的鏈表結構
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;}
}
構造函數與成員變量 變量主要有3個:
transient int size = 0;//當前列表的元素個數
/*** Pointer to first node.* Invariant: (first == null && last == null) ||* ? ? ? ? ? ?(first.prev == null && first.item != null)*/
transient Node<E> first;// 第一個元素
/*** Pointer to last node.* Invariant: (first == null && last == null) ||* ? ? ? ? ? ?(last.next == null && last.item != null)*/
transient Node<E> last;// 最后一個元素
在LinkedList中的構造函數有兩個,一個是無參的,另一個是帶Collection參數的。
public LinkedList() {}//無參構造函數
public LinkedList(Collection<? extends E> c) {this();addAll(c);//將c中的元素都添加到此列表中
}
其添加的過程中,此時size = 0,如下:
public boolean addAll(Collection<? extends E> c) {return addAll(size, c);//此時 size == 0
}
如果index==size,則添加c中的元素到列表的尾部;否則,添加的第index個元素的前面;
public boolean addAll(int index, Collection<? extends E> c) {// 檢查位置是否合法 位置是[0,size],注意是閉區間 否則報異常checkPositionIndex(index);Object[] a = c.toArray();// 得到一個元素數組int numNew = a.length;// c中元素的數量if (numNew == 0)return false;// 沒有元素,添加失敗// 主要功能是找到第size個元素的前驅和后繼。得到此元素需要分情況討論。// 這段代碼是各種情況的總和,可能有一點點容易懵逼。Node<E> pred, succ;// 前驅與后繼if (index == size) {// 如果位置與當前的size相同succ = null;// 無后繼pred = last;// 前驅為last,即第size個元素(最后一個元素)} else {// 若與size不同,即index位于[0, size)之間succ = node(index);// 后繼為第index個元素pred = succ.prev;// 前驅為后繼的前驅}// 后文有詳細的圖片說明// 開始逐個插入for (Object o : a) {@SuppressWarnings("unchecked") E e = (E) o;// 新建一個以pred為前驅、null為后繼、值為e的節點Node<E> newNode = new Node<>(pred, e, null);if (pred == null)// 前驅為空,則此節點被當做列表的第一個節點first = newNode;else// 規避掉了NullPointerException,感覺又達到了目的,又實現了邏輯pred.next = newNode;// 不為空,則將前驅的后繼改成當前節點pred = newNode;// 將前驅改成當前節點,以便后續添加c中其它的元素}// 至此,c中元素已添加到鏈表上,但鏈表中從size開始的那些元素還沒有鏈接到列表上// 此時就需要利用到之前找出來的succ值,它是作為這個c的整體后繼if (succ == null) {// 如果后繼為空,說明無整體后繼last = pred;// c的最后一個元素應當作為列表的尾元素} else {// 有整體后繼pred.next = succ;// pred即c中的最后一個元素,其后繼指向succ,即整體后繼succ.prev = pred;// succ的前驅指向c中的最后一個元素}// 添加完畢,修改參數size += numNew;modCount++;return true;
}
返回序號為index的元素節點??催@段代碼中的if語句,真的是佩服,這樣寫代碼,都可以這樣減少查找次數。
Node<E> node(int index) {// assert isElementIndex(index);// 這個地方很有意思。視其與中值得差距,覺得從前遍歷還是從后遍歷。if (index < (size >> 1)) {Node<E> x = first;// 循環index次 迭代到所需要的元素for (int i = 0; i < index; i++)x = x.next;return x;} else {Node<E> x = last;// 循環size-1-index次for (int i = size - 1; i > index; i--)x = x.prev;return x;}
}
測試代碼以及驗證輸出如下:
public class Main {public static void main(String[] args) {List<String> list = new LinkedList<>(Arrays.asList("1", "2", "3"));System.out.println(list.toString());list.addAll(2, Arrays.asList("4", "5"));System.out.println(list.toString());list.addAll(0, Arrays.asList("6", "7"));System.out.println(list.toString());}
}
---
[1, 2, 3]
[1, 2, 4, 5, 3]
[6, 7, 1, 2, 4, 5, 3]
增加元素 對于向列表中添加元素,先看一組基本的添加操作,具體如下:
將e鏈接成列表的第一個元素 源代碼以及相應的分析如下:
private void linkFirst(E e) {final Node<E> f = first;// 前驅為空,值為e,后繼為ffinal Node<E> newNode = new Node<>(null, e, f);first = newNode;// first指向newNode// 此時的f有可能為nullif (f == null)// 若f為空,則表明列表中還沒有元素last = newNode;// last也應該指向newNodeelsef.prev = newNode;// 否則,前first的前驅指向newNodesize++;modCount++;
}
其過程大致如下兩圖所示: 初始狀態:
后續狀態: 添加元素作為第一個元素時,所需要做的工作,有下列所述: 首先,獲取第一個節點,然后將該節點的前驅指向新添加的元素所在的節點; 接著,將新添加的節點的后繼指向前第一個節點; 最后,將first指向新添加的元素的節點。添加完畢。
將e鏈接為最后一個元素 源代碼以及相應的解釋如下:
void linkLast(E e) {final Node<E> l = last;// 找到最后一個節點// 前驅為前last,值為e,后繼為nullfinal Node<E> newNode = new Node<>(l, e, null);last = newNode;// last一定會指向此節點if (l == null)// 最后一個節點為空,說明列表中無元素first = newNode;// first同樣指向此節點elsel.next = newNode;// 否則,前last的后繼指向當前節點size++;modCount++;
}
其操作過程與前述linkFirst()的過程類似,因此其替換后的示意圖如下:
將e鏈接到節點succ前 源代碼以及相應的解析如下:
void linkBefore(E e, Node<E> succ) {// assert succ != null;final Node<E> pred = succ.prev; // 找到succ的前驅// 前驅為pred,值為e,后繼為succfinal Node<E> newNode = new Node<>(pred, e, succ);// 將succ的前驅指向當前節點succ.prev = newNode;if (pred == null)// pred為空,說明此時succ為首節點first = newNode;// 指向當前節點elsepred.next = newNode;// 否則,將succ之前的前驅的后繼指向當前節點size++;modCount++;
}
這個操作有點類似將上述的兩個操作整合到一起。其操作簡圖如下:
有了上述的分析,我們再來看一些添加的操作,這些操作基本上是做了一些邏輯判斷,然后再調用上述三個方法去實現添加功能,這里略過就好。
?public boolean add(E e) {linkLast(e);return true;}// 只有這個是有一點邏輯的public void add(int index, E element) {checkPositionIndex(index);if (index == size)// 為最后一個節點,當然是添加到最后一個~linkLast(element);elselinkBefore(element, node(index));}public void addFirst(E e) {linkFirst(e);}public void addLast(E e) {linkLast(e);}
刪除元素 刪除就是添加過程的逆過程。同樣,在分析我們使用的接口前,先分析幾個我們看不到的方法,如下:
刪除首節點
private E unlinkFirst(Node<E> f) {// assert f == first && f != null;別忽略這里的斷言final E element = f.item;// 取出首節點中的元素final Node<E> next = f.next;// 取出首節點中的后繼f.item = null;f.next = null; // help GCfirst = next;// first指向前first的后繼,也就是列表中的2號位if (next == null)// 如果此時2號位為空,那么列表中此時已無節點last = null;// last指向nullelsenext.prev = null;// 首節點無前驅size--;modCount++;return element;// 返回首節點保存的元素值
}
刪除尾節點 此處的操作與刪除首節點的操作類似。
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;別忽略這里的斷言
final E element = l.item;// 取出尾節點中的元素
final Node<E> prev = l.prev;// 取出尾節點中的后繼
l.item = null;
l.prev = null; // help GC
last = prev;// last指向前last的前驅,也就是列表中的倒數2號位
if (prev == null)// 如果此時倒數2號位為空,那么列表中已無節點first = null;// first指向null
elseprev.next = null;// 尾節點無后繼
size--;
modCount++;
return element;// 返回尾節點保存的元素值
}
刪除某個非空節點 這個也類似添加元素時的第三個基本操作,與結合了上述兩個操作有點類似。
// x即為要刪除的節點
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;// 保存x的元素值
final Node<E> next = x.next;// 保存x的后繼
final Node<E> prev = x.prev;// 保存x的前驅if (prev == null) {// 前驅為null,說明x為首節點first = next;// first指向x的后繼
} else {prev.next = next;// x的前驅的后繼指向x的后繼,即略過了xx.prev = null;// x.prev已無用處,置空引用
}if (next == null) {// 后繼為null,說明x為尾節點last = prev;// last指向x的前驅
} else {next.prev = prev;// x的后繼的前驅指向x的前驅,即略過了xx.next = null;// x.next已無用處,置空引用
}x.item = null;// 引用置空
size--;
modCount++;
return element;// 返回所刪除的節點的元素值
}
有了上面的幾個函數作為支撐,我們再來看下面的幾個我們能用來刪除節點的方法,他們也基本上是在一些邏輯判斷的基礎之上,再調用上述的基本操作:
public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);
}
public E removeLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return unlinkLast(l);
}
// 遍歷列表中所有的節點,找到相同的元素,然后刪除它
public boolean remove(Object o) {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) {if (o.equals(x.item)) {unlink(x);return true;}}}return false;
}
public E remove(int index) {checkElementIndex(index);return unlink(node(index));
}
修改元素 通過遍歷,循環index次,獲取到相應的節點后,再通過節點來修改元素值。
public E set(int index, E element) {checkElementIndex(index);Node<E> x = node(index);// 獲取到需要修改元素的節點E oldVal = x.item;// 保存之前的值x.item = element;// 修改return oldVal;// 返回修改前的值}
?
查詢元素
通過位置,循環index次,獲取到節點,然后返回該節點中元素的值public E get(int index) {checkElementIndex(index);return node(index).item;// 獲取節點,并返回節點中的元素值
}
?
還有兩個獲取首尾節點的元素的方法:public E getFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return f.item;
}
public E getLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return l.item;
}
?
獲取元素位置
從0開始往后遍歷public int indexOf(Object o) {int index = 0;if (o == null) {// 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;// 未找到,返回-1
}
?
從size - 1開始遍歷?;静僮髋c上述操作類似,只是起始位置不同。public int lastIndexOf(Object o) {int index = size;if (o == null) {for (Node<E> x = last; x != null; x = x.prev) {index--;if (x.item == null)return index;}} else {for (Node<E> x = last; x != null; x = x.prev) {index--;if (o.equals(x.item))return index;}}return -1;
}
額外的話 在上面的諸多函數中,有許多是需要進行位置判斷的。在源碼中,位置判斷有兩個函數,一個是下標,一個是位置??吹竭@兩個函數,確實是有一些感觸,這確實是需要比較強的總結能力以及仔細的觀察能力。
// 下標,保證數組訪問不越界。
private boolean isElementIndex(int index) {return index >= 0 && index < size;
}
// 位置
private boolean isPositionIndex(int index) {return index >= 0 && index <= size;
}
后記 LinkedList還實現了Queue這個接口,在實現這些接口時,仍然是做一些邏輯處理,然后調用上面所描述的基本操作,如link()、unlink()之類的,因此不再分析。還有其中的關于序列化、Iterator這兩塊,與ArrayList的實現也是不盡相同的,故在此可參考ArrayList中的解析。 ?
?
總結
以上是生活随笔 為你收集整理的LinkedList源码阅分析 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。