上一節咱們手寫實現了單鏈表和雙鏈表,本節咱們來看看源碼是如何實現的而且對比手動實現有哪些可優化的地方。html
經過上一節咱們對雙鏈表原理的講解,同時咱們對照以下圖也可知道雙鏈表算法實現有以下特色。java
一、鏈表中的每一個連接都是一個對象(也稱爲元素,節點等)。
二、每一個對象都包含一個引用(地址)到下一個對象的位置。
三、鏈表中前驅節點指向null表示鏈表的頭,鏈表中的後繼節點指向null,表示鏈表的尾。
四、連接列表能夠在運行時(程序運行時,編譯後)動態增加和縮小,僅受可用物理內存的限制。node
咱們首先看看LinkedList給咱們提供了哪些可操做的方法,以下:算法
LinkedList<String> list = new LinkedList(); //添加元素到尾結點 list.add("str"); //添加元素到指定索引節點 list.add(1,"str"); //添加元素到頭節點 list.addFirst("first"); //添加元素到尾節點 list.addLast("last"); //返回指定索引元素 list.get(1); //返回頭節點元素 list.getFirst(); //返回尾節點元素 list.getLast(); //添加元素到尾結點 list.offer("str"); //添加元素到頭節點 list.offerFirst("str"); //添加元素到尾節點 list.offerLast("str");
首先咱們來看看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; } }
接下來咱們再來看看對鏈表的定義,以下:安全
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { //鏈表長度 transient int size = 0; //鏈表頭節點 transient Node<E> first; //鏈表尾節點 transient Node<E> last; //構造函數 public LinkedList() { } //構造函數傳入集合進行轉換 public LinkedList(Collection<? extends E> c) { this(); addAll(c); } }
上述咱們所定義的雙鏈表類繼承了對應的類,實現了對應的接口,咱們經過一張圖來進行概括,以下:dom
緊接着經過如上一張圖以及上一節咱們對雙鏈表的基本原理講解,咱們首先給出源碼中雙鏈表特色,以下:函數
一、能夠用做隊列,雙端隊列或棧。由於已實現了List可做爲隊列,同時也實現了Queue和Deque接口可做爲雙端隊列或棧。
二、容許全部元素且可包含重複和NULL。
三、LinkedList維護元素的插入順序。
四、非線程安全。 若是多個線程同時訪問鏈表,而且如有一個線程在結構上修改了列表,則必須在外部進行同步,使用Collections.synchronizedList(new LinkedList())獲取同步的鏈表。
五、迭代器支持快速失敗(Fail-Fast),可能會拋出ConcurrentModificationException。
六、未實現RandomAccess接口。因此它不支持隨機訪問元素,咱們只能按順序訪問元素。源碼分析
因爲上一節咱們實現了其基本原理,而在java中實現雙鏈表只不過是給咱們提供了更多可操做的方法,好比在頭結點、尾節點插入元素是同樣的,這裏咱們就不一一貼出源碼了,針對在指定索引插入元素這點比咱們處理的好,上一節咱們對指定索引插入元素採起循環遍歷的方式,同時咱們只用到了後繼節點,而在java中,它是如何處理的呢,咱們下面來分析其源碼:性能
//指定索引插入元素 public void add(int index, E element) { //檢查插入索引位置是否超過邊界,不然拋出異常 checkPositionIndex(index); //若是插入索引和鏈表長度相等則插入尾節點 if (index == size) linkLast(element); else //不然在指定索引插入元素 linkBefore(element, node(index)); }
接下來咱們繼續看看若不是插入尾節點,那麼插入指定索引位置,它是如何處理的呢?
//在節點succ後插入元素e void linkBefore(E e, Node<E> succ) { //定義succ節點的前驅節點 final Node<E> pred = succ.prev; //實例化要插入元素的節點 final Node<E> newNode = new Node<>(pred, e, succ); //插入元素的前驅節點即爲succ succ.prev = newNode; if (pred == null) //若是succ的前驅爲null,說明爲頭節點則插入元素的節點爲頭節點 first = newNode; else //不然待插入指定索引的節點的後繼節點爲插入元素的節點 pred.next = newNode; size++; modCount++; } //獲取指定索引的節點 Node<E> node(int 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; } }
由上看出咱們上一節手動指定索引插入元素,在java中處理的更爲巧妙且性能更好,由於經過指定索引和鏈表的長度的二分之一做爲判斷依據能更快查找到指定索引節點。經過查看源碼咱們會發現add對應offer方法都是插入到尾節點,而addFirst對應offerFirst都是插入到頭節點,addLast對應offerLast都是插入到尾節點。詳細區別請參考園友文章,這裏我就不列舉了。http://www.javashuo.com/article/p-epzovtkp-hd.html。只要咱們瞭解了算法基本原理,再去看看源碼其實很簡單,只不過是對照着看看是否有更好的實現方式而已,關於源碼分析咱們就到此爲止。這裏咱們須要強調下雙鏈表的特色有一個是快速失敗機制及Fail-Fast,這裏咱們經過例子來講明。
快速迭代失敗 - 當咱們嘗試在迭代時修改集合的內部結構時,它會拋出ConcurrentModificationException,是失敗的快速迭代器。讓咱們來看以下例子
LinkedList<String> listObject = new LinkedList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); Iterator it = listObject.iterator(); while (it.hasNext()) { listObject.add("raju"); System.out.println(it.next()); }
在進一步討論以前,咱們分析爲何會出現此異常,咱們注意到從next()方法開始的異常堆棧問題。 在next()方法中,還有另外一個方法checkForComodification(),它根據某些條件拋出了ConcurrentModificationException異常,接下來 讓咱們看看next()和checkForComodification()方法中發生了什麼。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }
在checkForComodification()方法內部,若是modcount不等於expectedModCount則拋出ConcurrentModificationException(擴展RuntimException)。根據上述狀況,當咱們在循環中執行listObject.add(「raju」)時,modCount的值變爲6可是expectedCount的值仍然是5,這就是咱們獲得ConcurrentModificationException的緣由。因此看來咱們迭代時在next()方法中遇到了問題,顯然這種處理方式毫無疑問是正確的, 若是咱們不使用next方法打印元素,咱們使用加強的for循環試試。
LinkedList<String> listObject = new LinkedList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); for (String s : listObject ){ listObject.add("raju"); System.out.println(s); }
經過使用加強的for循環咱們仍然獲得ConcurrentModificationException異常,這是爲何呢?咱們沒有使用Iterator進行迭代啊,經過如上堆棧信息咱們知道,即便咱們使用加強的for循環,內部的next()方法也會被調用。在JDK1.5中,一些新類(CopyOnWriteArrayList,CopyOnWriteArraySet,ConcurrentHashMap等)不會拋出ConcurrentModificationException,即便咱們在迭代時修改List,Set或Map的結構。 這些類用於克隆或複製集合Object。
List<String> listObject = new CopyOnWriteArrayList<>(); listObject.add("ram"); listObject.add("mohan"); listObject.add("shyam"); listObject.add("mohan"); listObject.add("ram"); Iterator it = listObject.iterator(); while (it.hasNext()) { listObject.add("raju"); System.out.println(it.next()); }
接下來咱們總結出Fail-Fast和Fail Safe的區別所在,以下:
序號 | Fail Fast | Fail Safe |
---|---|---|
1. | Fail fast應用於集合對象 | 應用於克隆或集合對象的副本。 |
2. | 當迭代時,咱們對集合元素進行修改時將拋出ConcurrentModificationException異常 | 不會拋出任何異常 |
3. | 須要更少的內存 | 須要額外的內存 |
4. | ArrayList,HashSet, HashMap等等被使用時 | CopyOnWriteArrayList,ConcurrentHashMap等等類被使用時 |
咱們來看看在java中是如何將數組轉換爲LinkedList或將Linkedlist如何轉換爲數組的呢。
LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("A"); linkedList.add("B"); linkedList.add("C"); linkedList.add("D"); //1. LinkedList to Array String array[] = new String[linkedList.size()]; linkedList.toArray(array); System.out.println(Arrays.toString(array)); //2. Array to LinkedList LinkedList<String> linkedListNew = new LinkedList<>(Arrays.asList(array)); System.out.println(linkedListNew);
咱們可使用Collections.sort()方法對連接列表進行排序,如果對於對象的自定義排序,咱們可使用Collections.sort(連接的List,比較器)方法。以下:
LinkedList<String> linkedList = new LinkedList<>(); linkedList.add("A"); linkedList.add("C"); linkedList.add("B"); linkedList.add("D"); //Unsorted System.out.println(linkedList); //1. Sort the list Collections.sort(linkedList); //Sorted System.out.println(linkedList); //2. Custom sorting Collections.sort(linkedList, Collections.reverseOrder()); linkedList.sort(Collections.reverseOrder()); //Custom sorted System.out.println(linkedList);
在Java LinkedList類中,操做很快,由於不須要發生轉換,基本上,全部添加和刪除方法都提供了很是好的性能O(1)。LinkedList經常使用方法時間複雜度總結以下:
- add(E element) :O(1).
- get(int index) 和add(int index, E element) : O(N).
- remove(int index) :O(N).
- Iterator.remove() : O(1).
- ListIterator.add(E element) : O(1).
前面咱們也詳細分析了ArrayList源碼,以下咱們詳細分析比較下LinkedList和ArraryList,以下:
一、ArrayList使用動態擴容來調整數組大小。而LinkedList使用雙向鏈表實現。
二、ArrayList容許隨機訪問它的元素,而LinkedList則不容許,只能按順序訪問鏈表中的節點,所以訪問特定節點會很慢。
三、LinkedList也實現了Queue接口,它添加了比ArrayList更多的方法,例如offer,peek,poll等。
四、與LinkedList相比,ArrayList在添加和刪除方面較慢,但在get中更快,由於若是在LinkedList中數組已滿,則無需調整數組大小並將內容複製到新數組。
五、LinkedList比ArrayList具備更多的內存開銷,由於在ArrayList中,每一個索引僅保存實際對象,但在LinkedList的狀況下,每一個節點都保存後繼節點和前驅節點的數據和地址。
綜上所述LinkedList貌似毫無用武之地,那麼咱們究竟何時能夠用LinkedList呢?我認爲可從如下三方面考慮
一、不須要隨機訪問任何特定元素。
二、須要頻繁進行插入和刪除。
三、不肯定鏈表中有多少項。
本節咱們稍微分析了下LinkedList源碼,而後對比了ArrayList和LinkedList優缺點,以及咱們應當何時用LinkedList,除開極少數狀況,大部分狀況下都不太會用到LinkedList,好了本節咱們到這裏爲止,下一節咱們進入Hashtable的學習。感謝您的閱讀,下節見。