List家族中共兩個常用的對象ArrayList和LinkedList,具有以下基本特征。
- ArrayList:長於隨機訪問元素,中間插入和移除元素比較慢,在插入時,必須創建空間並將它的所有引用向前移動,這會隨着ArrayList的尺寸增加而產生高昂的代價,底層由數組支持。
- LinkedList:通過代價較低的在List中間進行插入和刪除操作,只需要鏈接新的元素,而不必修改列表中剩余的元素,無論列表尺寸如何變化,其代價大致相同,提供了優化的順序訪問,隨機訪問相對較慢,特性較ArrayList更大,而且還添加了可以使其作為棧、隊列或雙端隊列的方法,底層由雙向鏈表實現。
上面的特性接觸了Java開發的人都知道,初級Java工程師面試更是必問的點,至於更加深入的知識點:為什么會有這樣的特性? 底層是怎么實現的?往往到這里,大部分程序員都答不出來,筆者當初面試那會也是被這里吊打,被面試官無情的嘲諷,當時的情形是這樣的

當時那個臊的啊,拿回簡歷出了公司坐在深圳某天橋思考了許久的人生。。。。。。。。
抽空看了下LinkedList的實現,整理成博文的形式,能給自己增加理解,也希望能幫助廣大碼農。
LinkedList本質是一個雙向鏈表,由一個個的Node對象組成,如下圖

LinkedList由一個個Node組成,每一個Node持有前后節點的引用,也可以稱之為指針,看下Node的結構,Node有當前元素對象、前一個節點信息、后一個節點信息三個屬性,構造函數也是由這三個屬性去組成,在LinkedList的添加方法中,會創建一個Node對象,持有前后節點的信息,添加到最后節點之后。
//關於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; } }
先看下LinkedList中共的幾個屬性,下面三個屬性分別描述LinkedList的尺寸、第一個元素、最后一個元素,這三個元素始終貫穿着在LinkedList的使用當中
//transient 保證以下幾個屬性不被序列化 /** * The number of times this list has been <i>structurally modified</i>. * 該字段表示list結構上被修改的次數。結構上的修改指的是那些改變了list的長度 * 大小或者使得遍歷過程中產生不正確的結果的其它方式。 * */ protected transient int modCount = 0; 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的添加實現,可以添加為null的對象
public boolean add(E e) { linkLast(e); return true; } /** * Links e as last element. */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; }
看上面的源碼可以知道,當LinkedList添加一個元素時,會默認的往LinkedList最后一個節點后添加,具體步驟為
- 獲得最后一個節點last作為當前節點l
- 用當前節點l、添加參數e、null創建一個新的Node對象
- 將新創建的Node對象鏈接到最后節點,也就是last
- 如果當前的LinkedList為空,那么添加的node就是first,也是last
- 當前LinkedList的size+1,表示結構改變次數的對象modCount+1
整個添加過程中,系統只做了兩件事情,添加一個新的節點,然后保存該節點和前節點的引用關系。
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; }
LinkedList的刪除中,會根據傳遞進來的參數進行null判斷,因為在LinkedList的元素移除中,null和非null的處理不一樣,對於nul使用==去判斷是否匹配,對於非null使用.equals(Object o)去判斷,至於==和equals的區別,讀者自行百度。
null判斷之后,傳入當前節點調用unlink(E e)方法,我們可以從方法名中看出點東西,可能設計者也是為了方便讀者閱讀源碼,可以理解為“放開鏈接”,也可以反映出LinkedList保存數據的方式,一個個元素相互鏈接而成。
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; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
通過閱讀上面的源碼,LinkedList的刪除中,也是改變節點之間的引用關系去實現的,具體邏輯整理如下:
- 如果前一個節點prev為null,即第一個節點元素,則鏈表first = x下一個節點;
- 如果前一個節點prev不為null,即不是第一個節點元素,則將當前節點的next賦值給prev.next,x.prev置為null,也就是當前節點x的prev和next和當前鏈表中的其他元素不存在任何聯系;
- 如果下一個節點next為null,即為最后一個元素,則鏈表last = x前一個節點;
- 如果下一個節點next不為null,即不為最后一個元素,則將當前節點的prev賦值給next.prev,同樣當前節點x的prev和next和當前鏈表中的其他元素不存在任何聯系;
LinkedList的查詢實現源碼如下,
public E get(int index) { checkElementIndex(index); return node(index).item; } private void checkElementIndex(int index) { if (!isElementIndex(index)) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } private boolean isElementIndex(int index) { return index >= 0 && index < size; } Node<E> node(int index) { // assert isElementIndex(index); if (index < (size >> 1)) { Node<E> x = first; for (int i = 0; i < index; i++) x = x.next; return x; } else { Node<E> x = last; for (int i = size - 1; i > index; i--) x = x.prev; return x; } }
通過閱讀源碼,我們也可以解讀出LinkedList的查詢邏輯
- 根據傳入的index去判斷是否為LinkedList中的元素,判斷邏輯為index是否在0和size之間,如果在則調用node(index)方法,否則拋出IndexOutOfBoundsException;
- 調用node(index)方法,將size右移1位,即size/2,判斷傳入的size在LinkedList的前半部分還是后半部分
- 如果在前半部分,即index < size/2,則從fisrt節點開始遍歷匹配
- 如果在后半部分,即index > size/2,則從last節點開始遍歷匹配
可以看出,如果LinkedList鏈表size越大,則遍歷的時間越長,查詢所需的時間也越長。
應該可以將LinkedList的添加、刪除、查詢給說清楚,如果讀者還有不清楚或者覺得有什么不對的地方,歡迎各位指正或者入群交流。