一、前言
乍眼一看會懷疑或者問LinkedHashMap與HashMap有什么區別? 它有什么與眾不同之處? 由於前面已經有兩篇文章分析了HashMap,今天就看看LinkedHashMap。(基於JDK8)
二、結構屬性分析
1、繼承關系
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap是HashMap的子類,說明HashMap有的功能LinkedHashMap都有。
2、Entery<K, V> head、tail : 雙向鏈表
/** * 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; // Entry沒什么特別之處,都是調用父類創建節點的。 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); } }
3、accessOrder:如果為true,則表示訪問有序(新訪問的數據會被移至到鏈尾)。如果為false,表示插入有序。
/** * The iteration ordering method for this linked hash map: <tt>true</tt> * for access-order, <tt>false</tt> for insertion-order. * @serial */ private final boolean accessOrder;
這個字段的默認的值是false, 可以從構造函數中看出, 當然也可以指定。如下:
public LinkedHashMap() { super(); accessOrder = false; } // 指定accessOrder public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
那么什么是插入有序和訪問有序呢?都知道在HashMap中是插入或者訪問都是無序的。下面我們先通過實例看下這兩種情況的效果:
/** * 驗證插入有序 */ @Test public void test_accessOrder_false() { // accessOrder 默認為false,表示插入有序 Map<String, String> map = new LinkedHashMap<>(); map.put("玉樹臨楓", "本文作者"); map.put("Andy", "劉德華"); map.put("eson", "陳奕迅"); map.put("張三", "張三"); for(Map.Entry<String, String> entry : map.entrySet()) { System.out.println("key:" + entry.getKey()); } }
output: 看下面輸出結果,從而知道插入有序表示插入的時間順序,跟隊列的插入順序一樣:先進先出。(如果是HashMap輸出是亂序的。)
key:玉樹臨楓, value:本文作者
key:Andy, value:劉德華
key:eson, value:陳奕迅
key:張三, value:張三
接下來看下訪問有序是什么樣的:
/** * 測試訪問有序 */ @Test public void test_accessOrder_true() { // 指定accessOrder = true Map<String, String> map = new LinkedHashMap<>(10, 0.75f, true); map.put("玉樹臨楓", "本文作者"); map.put("Andy", "劉德華"); map.put("eson", "陳奕迅"); map.put("閱讀本文的你", "感謝你的支持"); for(Map.Entry<String, String> entry : map.entrySet()) { System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue()); } System.out.println("---------對Andy進行了采訪-------------"); map.get("Andy"); for(Map.Entry<String, String> entry : map.entrySet()) { System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue()); } System.out.println("--------------添加一位成員----------------"); map.put("James", "23"); for(Map.Entry<String, String> entry : map.entrySet()) { System.out.println("key:" + entry.getKey() + ", value:" + entry.getValue()); } }
output: 通過結果可以看出,不過是put操作還是get操作,都會將當前元素移至到鏈尾。
key:玉樹臨楓, value:本文作者 key:Andy, value:劉德華 key:eson, value:陳奕迅 key:閱讀本文的你, value:感謝你的支持 ---------對Andy進行了采訪------------- key:玉樹臨楓, value:本文作者 key:eson, value:陳奕迅 key:閱讀本文的你, value:感謝你的支持 key:Andy, value:劉德華 --------------添加一位成員---------------- key:玉樹臨楓, value:本文作者 key:eson, value:陳奕迅 key:閱讀本文的你, value:感謝你的支持 key:Andy, value:劉德華 key:James, value:23
好奇的朋友肯定想知道它是怎樣做到這樣的特性, 還是得從源碼角度去看看。
三、重要函數分析
1、put函數
其實這個函數我們已經在上篇已經分析過了,那么為什么還來看呢? 因為LinkedHashMap是HashMap的子類啊,這些都是繼承使用的。但有沒有發現其中有什么需要注意的呢? 再次看下put函數的源碼加深下印象。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table為空,則通過擴容來創建,后面在看擴容函數 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 根據key的hash值 與 數組長度進行取模來得到數組索引 if ((p = tab[i = (n - 1) & hash]) == null) // 空鏈表,創建節點 tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; // 不為空,則判斷是否與當前節點一樣,一樣就進行覆蓋 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) // 不存在重復節點,則判斷是否屬於樹節點,如果屬於樹節點,則通過樹的特性去添加節點 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { // 該鏈為鏈表 for (int binCount = 0; ; ++binCount) { // 當鏈表遍歷到尾節點時,則插入到最后 -> 尾插法 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 檢測是否該從鏈表變成樹(注意:這里是先插入節點,沒有增加binCount,所以判斷條件是大於等於閾值-1) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st // 滿足則樹形化 treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } // 存在相同的key, 則替換value if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; // 注意這里,這里是供子類LinkedHashMap實現 afterNodeAccess(e); return oldValue; } } ++modCount; // 注意細節:先加入節點,再加長度與閾值進行判斷,是否需要擴容。 if (++size > threshold) resize(); // 注意這里,這里是供子類LinkedHashMap實現 afterNodeInsertion(evict); return null; }
注意上面邏輯:
- 每次插入都會調用newNode函數創建一個新節點,對於LinkeHashMap來說有重寫該函數。
- 當存在相同key替換value后,會調用afterNodeAccess函數,這函數在HashMap中是沒有任何實現的,主要是供子類LinkeHashMap來實現。
// Callbacks to allow LinkedHashMap post-actions void afterNodeAccess(Node<K,V> p) { }
- 當擴容完后,會調用afterNodeInsertion函數,同理這個函數也是供子類LinkeHashMap來實現的。
void afterNodeInsertion(boolean evict) { }
2、newNode()函數
我們看看LinkedHashMap中的newNode()函數的實現,看看多了些什么功能有什么作用。
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; } 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; } }
從上面看來, LinkedHashMap不僅擁有HashMap的結構和功能,還額外的維護了一套雙向鏈表。另外其插入動作的順序也知道了:
put() -> putVal() -> newNode() -> linkNodeLast
3、afterNodeAccess函數
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // 如果accessOrder=true,即訪問有序,且雙向鏈表不止一個節點的時候,進行下面操作: if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 將p的后置指針置為null p.after = null; // 如果e的前置指針沒有元素, 則直接將雙向鏈表的頭節點指向它。 if (b == null) head = a; else // e的前置指針存在元素, 則將e的前置指針指向節點的后置指針指向其后置指針指向的的節點。 b.after = a; // e的后置指針存在元素, 則將e的后置指針指向節點的前置指針指向e前置指針指向的節點 if (a != null) a.before = b; else // 否則將尾節點指向e的前置節點 last = b; // 上面步驟主要是將e節點從鏈表中移除,然后添加到鏈表尾部 if (last == null) head = p; else { // 添加置鏈表尾部 p.before = last; last.after = p; } tail = p; ++modCount; } }
從上面函數分析可以看出來,當訪問到雙向鏈表存在的值時,如果開啟訪問有序的開關,則會將訪問到的節點移至到雙向鏈表的尾部。另外get函數也會調用這個函數,所以從源碼的角度去看問題很清晰。
public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; // 如果存在節點且開啟了訪問有序的開關,則會將當前節點移至雙向鏈表尾部 if (accessOrder) afterNodeAccess(e); return e.value; }
4、afterNodeInsertion函數
該函數表示是否需要刪除最年長的節點
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); } } // 返回false, 所以LinkedHashMap不會有刪除年長節點的行為,但其子類可以繼承重寫該函數。 protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
看這個功能有沒有想起和某些功能類似呢? 比如LRUCache : 最近最少使用的緩存淘汰策略。
5、Entry下的forEach函數
public final void forEach(Consumer<? super Map.Entry<K,V>> action) { if (action == null) throw new NullPointerException(); int mc = modCount; // 遍歷的是雙向鏈表。所以我們看到的就是插入的順序 for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) action.accept(e); if (modCount != mc) throw new ConcurrentModificationException(); }
四、總結
- LinkedHashMap 擁有與 HashMap 相同的底層哈希表結構,即數組 + 單鏈表 + 紅黑樹,也擁有相同的擴容機制。
- LinkedHashMap 相比 HashMap 的拉鏈式存儲結構,內部額外通過 Entry 維護了一個雙向鏈表。
- HashMap 元素的遍歷順序不一定與元素的插入順序相同,而 LinkedHashMap 則通過遍歷雙向鏈表來獲取元素,所以遍歷順序在一定條件下等於插入順序。
- LinkedHashMap 可以通過構造參數 accessOrder 來指定雙向鏈表是否在元素被訪問后改變其在雙向鏈表中的位置。