jdk原始碼閱讀筆記-LinkedHashMap
Map是Java collection framework 中重要的組成部分,特別是HashMap是在我們在日常的開發的過程中使用的最多的一個集合。但是遺憾的是,存放在HashMap中元素都是無序的,原因是我們在put或get資料的時候都是根據key的hash值來確定元素的位置。在具體的業務場景中,我們更多的希望對於HashMap集合能夠進行順序訪問,好在 jdk 中已經給我們提供了一種解決方案,那就是LinkedHashMap。該類繼承與HashMap,因此HashMap擁有的特性它都有,同時還具備其他的特性,比如實現了插入順序排序和訪問順序排序,預設以插入順序排序。同時也能夠利用LinkedHashMap實現LRU演算法。LinkedHashMap api很少,基本都是呼叫HashMap的方法,所以建議熟悉HashMap原始碼之後再來看這篇文章,我之前也寫過【
LRU演算法: LRU是Least Recently Used的縮寫,即最近最少使用,也就是說將熱點資料放到最前面,冷門資料放到最後,當達到一定條件後會刪除冷門資料,在一個快取系統中經常會用到該演算法。
一、LinkedHashMap與HashMap資料結構對比
從上圖可以看到HashMap的資料結構位陣列+單向連結串列,資料存放在連結串列的node節點上,每個node節點上都有一個指標指向下一個節點,每個陣列index上的連結串列跟其他的index上面的連結串列是部相互連結的。LinkedHashMap在部破壞HashMap的結構基礎之上,每個node節點都額外增加了兩個指標,分別指向了前一個節點和下一個節點,所以在HashMap上所有的node節點形成了一條雙向連結串列,每次新增往LinkedHashMap put資料的時候都將節點放在雙向連結串列的最後位置,從而實現了插入順序排序。在LinkedHashMap中,節點的定義如下:
/** * HashMap.Node subclass for normal LinkedHashMap entries. */ 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內部類,但是又額外添加了兩個屬性,before和after,分別指向前一個節點和後一個節點,形成一個雙向連結串列。
二、LinkedHashMap類結構
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> { ....... }
LinkedHashMap繼承了HashMap,所以擁有HashMap的所有特性。
三、成員變數
/** * 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; /** * The iteration ordering method for this linked hash map: <tt>true</tt> * for access-order, <tt>false</tt> for insertion-order. * * @serial */ final boolean accessOrder;
LinkedHashMap在HashMap的基礎之上添加了head、tail和accessOrder屬性:
head:雙向連結串列的表頭
tail: 雙向連結串列的表尾
accessOrder:排序的標誌。預設為false,按插入順序排序。可以通過構造方法設定為true,按訪問順序排序。
四、構造方法
/** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the specified initial capacity and load factor. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } /** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the specified initial capacity and a default load factor (0.75). * * @param initialCapacity the initial capacity * @throws IllegalArgumentException if the initial capacity is negative */ public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } /** * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance * with the default initial capacity (16) and load factor (0.75). */ public LinkedHashMap() { super(); accessOrder = false; } /** * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with * the same mappings as the specified map. The <tt>LinkedHashMap</tt> * instance is created with a default load factor (0.75) and an initial * capacity sufficient to hold the mappings in the specified map. * * @param m the map whose mappings are to be placed in this map * @throws NullPointerException if the specified map is null */ public LinkedHashMap(Map<? extends K, ? extends V> m) { super(); accessOrder = false; putMapEntries(m, false); } /** * Constructs an empty <tt>LinkedHashMap</tt> instance with the * specified initial capacity, load factor and ordering mode. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @param accessOrder the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
5個構造方法都是直接呼叫父類HashMap的構造方法。
五、新增資料put(Object key,V value)
LinkedHashMap中並沒有對HashMap進行復寫,也就是說新增資料的時候其實就是呼叫的HashMap的put方法,那麼它是怎麼進行排序的呢?下面我們一起來看看HashMap中是怎麼新增資料的吧。
public V put(K key, V value) { return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /** * 通過位與的方式來確定下標位置,判斷當前下標位置是否為空,如果為空直接放入到該位置上 * 不為空則通過equals方法來尋找當前位置上面的元素,如果有相同的key,則將覆蓋掉,如果沒有則將node放置在對應 * 位置上面 */ 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))))//已存在相同的key的資料,將其覆蓋 e = p; else if (p instanceof TreeNode)//當前位置是紅黑樹,將Node節點放到紅黑樹中 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);//建立新節點 //連結串列的長度超過轉換紅黑數的閾值,則將該連結串列轉成紅黑樹 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))))//覆蓋相同key的node break; p = e; } }
//map中已經存在了相同的key,將原來的資料替換掉 if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);//替換資料被視為更新資料,所以呼叫訪問排序方法。 return oldValue; } } ++modCount;//快速失敗機制 if (++size > threshold)//每次插入資料都要判斷一下當前儲存的資料是否需要擴容 resize(); afterNodeInsertion(evict);//插入資料後進行插入排序 return null; }
HashMap 插入資料的核心方法為 putVal方法,每次插入資料都呼叫newNode方法,這個方法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; }
第一:因為已經重寫了父類的newNode方法,所以在插入資料時建立新節點實際是呼叫了LinkedHashMap的newNode方法,該方法中每次建立新節點都想LinkedHashMap自身維護的雙向連結串列的尾部新增一個當前新創節點,我們繼續看看linkNodeLast方法:
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; } }
這個方法很簡單,如果連結串列為空則將新節點設定為頭部和尾部,否則將新節點放到連結串列的最後,將新節點的前指標指向原尾部的節點,原尾部節點的後指標指向新節點。如果不明白具體的插入流程,可參考我之前的【ArrayList原始碼閱讀筆記】,裡面有詳細插入各個位置的流程。
第二:建立新節點完成後,將新節點放入物件的連結串列或樹中,如果新節點的key在HashMap中已經存在,那麼就會原來的value覆蓋掉,此時被視為修改了節點,呼叫afterNodeAccess(e)方法,LinkedHashMap對這個方法進行了重寫,我們看一下原始碼:
/** * 每次訪問節點後將該節點放在最後 * @param e */ 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; } }
該方法中,如果 accessOrder 為true並且訪問節點不為空,那麼就會將訪問過的節點移動到最後。這也就是實現了LRU演算法,具體移動路程如下:
第三:插入資料全部完成之後,執行afterNodeInsertion(evict)方法,evict為true,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); } }
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
afterNodeInsertion方法是用來刪除最舊最少使用的資料,上面提到過,每次訪問、修改map中的資料的時候都會將該節點放在連結串列的最後面,因此約靠前的資料使用的頻率的越低,我們稱之為冷資料,該方法就是將最冷門的資料刪除掉。removeEldestEntry方法預設返回false,所以LinkedHashMap本身並不提供LRU演算法,需要自己手動實現LinkedHashMap,然後重寫removeEldestEntry方法,根據自己具體的業務決定何時刪除冷資料。
總結:這裡只總結LinkedHashMap實現部分。在put資料的時候,LinkedHashMap不僅將資料放在HashMap中,同時也將該資料放在自己維護的雙向連結串列的最後,以實現順序排序。如果put進去的資料的key已經存在與Map中,則將該資料移動到連結串列的最後位置。插入資料完成後,根據子類具體實現情況是否將第一個資料刪除。
六、獲取資料get(key)
/** * Returns the value to which the specified key is mapped, * or {@code null} if this map contains no mapping for the key. * * <p>More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code (key==null ? k==null : * key.equals(k))}, then this method returns {@code v}; otherwise * it returns {@code null}. (There can be at most one such mapping.) * * <p>A return value of {@code null} does not <i>necessarily</i> * indicate that the map contains no mapping for the key; it's also * possible that the map explicitly maps the key to {@code null}. * The {@link #containsKey containsKey} operation may be used to * distinguish these two cases. */ 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; }
這個方法看起來也比較簡單,如果accessOrder為true,即按訪問順序排序,那個每次都將該資料放到連結串列的最後面。
七、其他方法
LinkedHashMap本身的方法比較少,而且大部分都是呼叫父類的方法,所以在這裡就不說了,可以看看HashMap的原始碼。
八、總結
LinkedHashMap繼承與HashMap,因此它有HashMap一樣的特性。同時也彌補了HashMap無法順序遍歷的缺點。LinkedHashMap可以實現插入順序排序(預設排序),也可以根據訪問順序排序,也就是訪問的元素次數越多,該元素就越靠前。實現順序遍歷的底層原理是,LinkedHashMap自身維護了一張雙向連結串列,為此插入、訪問或修改資料的時候都將該節點放在連結串列最後面。按預設排序方式的話,在遍歷的時候就從表頭開始遍歷,按訪問順序排序就從連結串列表尾開始遍歷。另外,LinkedHashMap也可以用來搭建一個快取系統底層儲存結構,後面如果我有空的話,可能也會手寫一個簡單的快取demo。最後,如果文章有什麼寫的不對的地方,歡迎大家提出來,我的qq:1170971295。