HashMap做爲一種經典的數據結構,其根據key定位元素能達到平均O(1)的時間複雜度。 可是,存儲於HashMap中的元素顯然是無序的,遍歷HashMap的順序得看臉。。。
那如何使得HashMap裏的元素變得有序呢?一種思路是,將存放HashMap元素的節點,使用指針將他們串起來。換言之,就像在HashMap裏面「嵌入」了一個鏈表同樣。
實際上,jdk的LinkedHashMap就是使用這種思路實現的。java
<!-- more -->node
LinkedHashMap中的代碼不算多,這是由於,jdk的設計使用繼承複用了代碼,在jdk的設計中,LinkedHashMap是HashMap的擴展:算法
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> { /* ... */ }
回想一下HashMap的實現方式中,將key和value打包成的節點有兩種:
第一種,傳統分離鏈表法的鏈表節點。數據結構
static class Node<K,V> implements Map.Entry<K,V> { /* ... */ }
第二種,HashMap爲進行優化,必定狀況下會將鏈表重構爲紅黑樹。第二種節點是紅黑樹節點:函數
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { /* ... */ }
忽然發現,HashMap的TreeNode是繼承至LinkedHashMap的Entry的。。。
我的觀點是jdk這種作法不是很優雅,自己LinkedHashMap繼承HashMap就使得二者之間的邏輯混在了一塊兒,而這裏的內部實現又反過來繼承,邏輯搞得很混亂。優化
LinkedListHashMap須要將節點串成一個「嵌入式」雙向鏈表,所以須要給這兩種節點增長兩個字段:spa
static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
擴展HashMap.Node,增長雙向鏈表字段。
因爲TreeNode是繼承自LinkedListMap.Entry的,因此它也有這兩個字段。設計
再來看下LinkedHashMap中的屬性,不多:指針
/** * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> head; /** * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry<K,V> tail;
記錄雙向鏈表的表頭和表尾。從註釋中能夠看出,head節點是最老的,tail節點是最新的,也即鏈表按照由老到新的順序串起來。code
最後,因爲LinkedHashMap是能夠設置它組織元素的順序。一種是鏈表中的元素是按插入時候的順序排序,另一種是按照訪問的順序排序。
// 指定順序是按照訪問順序來,仍是插入順序來 final boolean accessOrder;
這個accessOrder
指定是否按插入順序來。
因爲對Map中的節點進行了擴展,所以,在建立節點時不能使用原來的節點了,而應該使用從新建立後的。
HashMap將建立節點的操做抽取出來放到了單獨的函數中,LinkedHashMap重寫便可:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; } Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; LinkedHashMap.Entry<K,V> t = new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next); transferLinks(q, t); return t; } // HashMap的TreeNode是繼承自LinkedHashMap.Entry的,所以可以參與組織雙向鏈表 TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next); linkNodeLast(p); return p; } TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) { LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p; TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next); transferLinks(q, t); return t; }
接下來,則須要在LinkedHashMap的操做時維護雙向鏈表。
回顧下HashMap的源代碼,咱們知道,HashMap在刪除節點後,會調用afterNodeRemoval
函數。
這個函數在HashMap中是空的,實際上jdk是將它設計爲一個hook,果真,在LinkedHashMap中,就重寫了該函數,在其中維護雙向鏈表:
// 當有節點被刪除(即有元素被移除),那麼也要將它從雙向鏈表中移除 void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
按照相似的思路,HashMap中在插入元素後會調用afterNodeInsertion
,那是否是LinkedHashMap也在這裏實現了相關邏輯,插入元素後維護雙向鏈表節點呢?
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
然而,實際上在LinkedHashMap中該函數彷佛沒有什麼用。由於:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
removeEldestEntry
始終返回false,afterNodeInsertion至關於什麼也沒作。這個邏輯設計目的是什麼,還不能很清楚。也許也是爲了讓誰去繼承?之後再探究。
那插入元素後的在哪裏維護了雙向鏈表呢?回到以前的newNode
和newTreeNode
:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; } TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) { TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next); linkNodeLast(p); return p; } // 尾插雙向鏈表節點 private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
因爲HashMap中調用newNode時候都是爲了裝新插入的元素,因此在這裏維護雙向鏈表。
感受耦合是否是太緊了。。。若是HashMap因爲某個操做須要臨時搞個newNode借用下,豈不是會出問題?
下面是replacementNode
和replacementTreeNode
。replacementNode
在HashMap中的做用是,該K V以前是被TreeNode包裝的,如今須要拿Node包裝它。這也勢必會影響雙向鏈表的結構,因此這裏也須要額外維護下。
獲取的時候,一樣,是重寫了`afterNodeAccess`鉤子,這樣在HashMap的獲取邏輯結束後,這裏的邏輯會被執行,維護雙向鏈表。 void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
LinkedHashMap中的順序有訪問序和插入序,只有訪問序才須要在訪問的時候更新雙向鏈表結構。也即accessOrder
爲true纔會執行這段邏輯。
最後,注意到:
++modCount; }
通常來講,只有修改了Map結構的操做,才須要修改modCount以讓正在迭代的迭代器感知到了變化。
可是這裏,因爲迭代器是使用這裏的「嵌入式」雙向鏈表進行迭代,而在這裏會改變雙向鏈表的結構,若迭代器繼續迭代會形成不可預測的結果。
因此,這裏須要改變modCount
,阻止迭代器繼續迭代。
LinkedHashMap的一個典型應用場景是LRU算法。
因爲如今夜已深,如今不敢熬夜身體吃不消,想睡覺了。因此這個坑之後再填
LinkedHashMap還有其它的一些實現細節,如:
clear
的時候也要同時維護雙向鏈表;最後,總結下jdk中對LinkedHashMap中的實現思路: