LinkedList
裏面涉及到的一些操做,很是細緻,以免出現的空指針,理解後對於其優勢與缺點會有一個更加總體的認識吧。
在LinkedList
中,每個元素都是Node
存儲,Node
擁有一個存儲值的item
與一個前驅prev
和一個後繼next
,以下:java
// 典型的鏈表結構 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個:node
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語句,真的是佩服,這樣寫代碼,均可以這樣減小查找次數。this
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; } }
測試代碼以及驗證輸出以下:spa
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]
對於向列表中添加元素,先看一組基本的添加操做,具體以下:3d
源代碼以及相應的分析以下:指針
private void linkFirst(E e) { final Node<E> f = first; // 前驅爲空,值爲e,後繼爲f final Node<E> newNode = new Node<>(null, e, f); first = newNode;// first指向newNode // 此時的f有可能爲null if (f == null)// 若f爲空,則代表列表中尚未元素 last = newNode;// last也應該指向newNode else f.prev = newNode;// 不然,前first的前驅指向newNode size++; modCount++; }
其過程大體以下兩圖所示:
初始狀態:
後續狀態:
添加元素做爲第一個元素時,所須要作的工做,有下列所述:
首先,獲取第一個節點,而後將該節點的前驅指向新添加的元素所在的節點;
接着,將新添加的節點的後繼指向前第一個節點;
最後,將first指向新添加的元素的節點。添加完畢。code
源代碼以及相應的解釋以下:
void linkLast(E e) { final Node<E> l = last;// 找到最後一個節點 // 前驅爲前last,值爲e,後繼爲null final Node<E> newNode = new Node<>(l, e, null); last = newNode;// last必定會指向此節點 if (l == null)// 最後一個節點爲空,說明列表中無元素 first = newNode;// first一樣指向此節點 else l.next = newNode;// 不然,前last的後繼指向當前節點 size++; modCount++; }
其操做過程與前述linkFirst()
的過程相似,所以其替換後的示意圖以下:
源代碼以及相應的解析以下:
void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; // 找到succ的前驅 // 前驅爲pred,值爲e,後繼爲succ final Node<E> newNode = new Node<>(pred, e, succ); // 將succ的前驅指向當前節點 succ.prev = newNode; if (pred == null)// pred爲空,說明此時succ爲首節點 first = newNode;// 指向當前節點 else pred.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); else linkBefore(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 GC first = next;// first指向前first的後繼,也就是列表中的2號位 if (next == null)// 若是此時2號位爲空,那麼列表中此時已無節點 last = null;// last指向null else next.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 else prev.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的後繼,即略過了x x.prev = null;// x.prev已無用處,置空引用 } if (next == null) {// 後繼爲null,說明x爲尾節點 last = prev;// last指向x的前驅 } else { next.prev = prev;// x的後繼的前驅指向x的前驅,即略過了x x.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
開始遍歷。基本操做與上述操做相似,只是起始位置不一樣。
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
中的解析。