前文傳送門:Java小白集合源碼的學習系列:ArrayListjava
本篇爲集合源碼學習系列的LinkedList
學習部分,若有敘述不當之處,還望評論區批評指正!node
LinkedList和ArrayList同樣,都實現了List接口,都表明着列表結構,都有着相似的add,remove,clear等操做。與ArrayList不一樣的是,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是指向第一個節點的指針。永遠只有下面兩種狀況: * 一、鏈表爲空,此時first和last同時爲空。 * 二、鏈表不爲空,此時第一個節點不爲空,第一個節點的prev指針指向空 */ transient Node<E> first; /** * last是指向最後一個節點的指針,一樣地,也只有兩種狀況: * 一、鏈表爲空,first和last同時爲空 * 二、鏈表不爲空,此時最後一個節點不爲空,其next指向空 */ transient Node<E> last; //須要注意的是,當first和last指向同一節點時,代表鏈表中只有一個節點。
瞭解基本屬性以後,咱們看看它的構造方法,因爲沒必要在意它存儲的位置,它的構造器也是至關簡單的:this
//建立一個空鏈表 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; }
再來看看,在鏈表中普通刪除元素的操做是怎麼樣的:code
//取消一個非空節點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++; }
咱們還知道,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; }
與之相似的還有:
若是有興趣的話,能夠研究一下,總之仍是相對簡單的。
而LinkedList底層基於雙向鏈表實現,不須要連續的內存存儲,經過節點之間相互引用地址造成聯繫。
對於無索引位置的插入來講,例如向後插入,時間複雜度近似爲O(1),體現出增刪操做較快。可是若是要在指定的位置上插入,仍是須要移動到當前指定索引位置,才能夠進行操做,時間複雜度近似爲O(n)。
Linkedlist不支持快速隨機訪問,查詢較慢。
線程不安全,一樣的,關於線程方面,之後學習時再進行總結。