LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基於拉鏈式散列結構。該結構由數組和鏈表+紅黑樹,在此基礎上LinkedHashMap 增加了一條雙向鏈表,保持遍歷順序和插入順序一致的問題。
訪問順序存儲的LinkedHashMap會把get方法對應的Entry節點放置在Entry鏈表表尾。
LinkedHashMap構造函數有3個參數:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
initialCapacity:是初始數組長度
loadFactor:負載因子,表示數組的元素數量/數組長度超過這個比例,數組就要擴容
accessOrder:false: 基於插入順序(默認) true: 基於訪問順序
當accessOrder為true,每次get元素的時候,都會去執行 afterNodeAccess 方法,這個方法會將元素重新插入到雙向鏈表的結尾。
//標准的如何在雙向鏈表中將指定元素放入隊尾 // LinkedHashMap 中覆寫 //訪問元素之后的回調方法 /** * 1. 使用 get 方法會訪問到節點, 從而觸發調用這個方法 * 2. 使用 put 方法插入節點, 如果 key 存在, 也算要訪問節點, 從而觸發該方法 * 3. 只有 accessOrder 是 true 才會調用該方法 * 4. 這個方法會把訪問到的最后節點重新插入到雙向鏈表結尾 */ void afterNodeAccess(Node<K,V> e) { // move node to last // 用 last 表示插入 e 前的尾節點 // 插入 e 后 e 是尾節點, 所以也是表示 e 的前一個節點 LinkedHashMap.Entry<K,V> last; //如果是訪問序,且當前節點並不是尾節點 //將該節點置為雙向鏈表的尾部 if (accessOrder && (last = tail) != e) { // p: 當前節點 // b: 前一個節點 // a: 后一個節點 // 結構為: b <=> p <=> a LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 結構變成: b <=> p <- a p.after = null; // 如果當前節點 p 本身是頭節點, 那么頭結點要改成 a if (b == null) head = a; // 如果 p 不是頭尾節點, 把前后節點連接, 變成: b -> a else b.after = a; // a 非空, 和 b 連接, 變成: b <- a if (a != null) a.before = b; // 如果 a 為空, 說明 p 是尾節點, b 就是它的前一個節點, 符合 last 的定義 // 這個 else 沒有意義,因為最開頭if已經確保了p不是尾結點了,自然after不會是null else last = b; // 如果這是空鏈表, p 改成頭結點 if (last == null) head = p; // 否則把 p 插入到鏈表尾部 else { p.before = last; last.after = p; } tail = p; ++modCount; } }
每當put元素后,都會執行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); } }
如果是自己繼承LinkedhashMap來實現一個LRUCache,則需要重寫removeEldestEntry方法,可以將最早訪問的元素(即雙向鏈表的頭結點)刪除。
@Override
//移除最近最少被訪問條件之一,通過覆蓋此方法可實現不同策略的緩存 protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { return this.size() > cache_size; }
總結
LinkedHashMap在HashMap的基礎上使用一個雙端鏈表維持有序的節點。這個有序並不是通常意義上的大小關系,默認情況下使用的插入順序,意味着新插入的節點被添加到雙端鏈表的尾部,而一旦使用了訪問順序,即accessOrder為true,那么在訪問某一節點時,會將該節點移到雙端鏈表的尾部。正因為此特性,可以在LinkedHashMap中使用三個參數的構造方法並制定accessOrder為true將LinkedHashMap實現為LRU緩存,這樣經常訪問的就會被移到鏈表的尾部,而越少訪問的就在鏈表的頭部。
由於雙端鏈表維持了所有的節點,所以keySet()、values()以及entrySet()得到的鍵、值、鍵值對都是按照雙端鏈表中的節點順序的。
另外尤其需要注意的是,在put、get、remove方法中涉及到的雙端鏈表的操作,由於都是引用的更改,所以並沒有影響到HashMap的底層結構:數組+鏈表+紅黑樹。