HashMap 有一個不足之處就是在迭代元素時與插入順序不一致。而大多數人都喜歡按順序作某些事情,因此,LinkedHashMap 就是針對這一點對 HashMap 進行擴展,主要新增了**「兩種迭代方式」**:java
LinkedHashMap 的重點就是這兩點,搞清楚了,也就掌握了。因爲它繼承自 HashMap,因此在分析源碼以前必定要先看看 HashMap 的源碼,可查看此文《深刻分析 JDK8 中 HashMap 的原理、實現和優化》。數組
在 JDK 8 中,由於 LinkedHashMap 子類的存在,HashMap 引入的紅黑樹,在普通模式和樹模式之間的轉換變得複雜,那麼它是如何設計和簡化的呢?接下來看看 JDK 8 中的 LinkedHashMap 源碼的設計和實現。緩存
HashMap 爲何在迭代時就雜亂無章了?看下它在迭代時獲取下一個元素的方式,你就明白了:安全
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next; // 下一個元素要訪問節點
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null) throw new NoSuchElementException();
// 首先迭代自身所在鏈表
if ((next = (current = e).next) == null && (t = table) != null) {
// 若是鏈表訪問結束,遍歷哈希桶數組中下一個非空元素
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
複製代碼
能夠看到,HashMap 是按照哈希桶數組中非空的槽依次遍歷的,再加上哈希衝突(鏈表)的狀況,與插入順序相比,更加亂了。數據結構
那 LinkedHashMap 怎麼解決的?它維護了一個貫穿全部元素的雙向鏈表,遍歷時,迭代器直接對雙向鏈表進行迭代,從而保證了與插入順序一致,關鍵成員屬性和節點信息定義以下:源碼分析
// 雙向鏈表頭節點,也是最久沒有訪問的元素
transient LinkedHashMap.Entry<K,V> head;
// 雙向鏈表尾節點,也是最近剛剛訪問的元素
transient LinkedHashMap.Entry<K,V> tail;
// 迭代方式,true-按訪問順序迭代,false-按插入順序迭代,默認 false
final boolean accessOrder;
// 添加了構建雙向鏈表的前驅和後繼指針
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 在元素插入、刪除和訪問時定義並調用了一些 Hook 方法,這些方法使得 LinkedHashMap 內部保持有序的機制相對獨立,下降了普通模式和樹模式轉換的複雜度。優化
此外,紅黑樹節點繼承的是 LinkedHashMap 的 Entry,當然可能多了兩個指針,但在實現時有助於避免操做指針出現混淆錯誤。spa
LinkedHashMap 在元素插入時,覆蓋的回調方法主要有 newNode, replacementNode, replacementTreeNode, newTreeNode,主要就是保持雙向鏈表,核心是下面兩個方法:線程
// 插入到雙向鏈表的尾部
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;
}
}
// 鏈接兩個節點之間的先後指針
private void transferLinks(LinkedHashMap.Entry<K,V> src, LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}
複製代碼
迭代器的實現就比較簡單了,直接對這個雙向鏈表迭代便可,此時的迭代順序就插入順序。設計
爲了讓元素按訪問順序排列,HashMap 定義瞭如下 Hook 方法,供 LinkedHashMap 實現:
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
複製代碼
各個方法實現的原理以下:
重點看下插入以後的實現:
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);
}
}
複製代碼
是否會刪除頭節點,是由 removeEldestEntry 方法決定的,默認返回 false。在覆蓋這個方法時,不能簡單的返回 true,由於這樣可能會致使一個空的 LinkedHashMap,一般的作法是在插入指定數量的元素後再刪除,具體見下面 LRU 緩存的實現。
藉助 LinkedHashMap 能夠很方便的實現一個 LRU 緩存數據結構,只需設置 accessOrder 爲 true,並覆蓋 removeEldestEntry 方法便可,代碼以下:
final int MAX_CACHE_SIZE = 100;
LinkedHashMap<Object, Object> lru = new LinkedHashMap<Object, Object>(MAX_CACHE_SIZE, 0.75f, true) {
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
return size() > MAX_CACHE_SIZE;
}
};
複製代碼
LinkedHashMap 代碼比較簡單,難的都被 HashMap 實現了:),它們的異同點分別以下:
看這個類的源碼,最主要的仍是看 HashMap 定義 Hook 方法,使得 LinkedHashMap 保持有序的機制相對獨立的設計,這是模板模式的應用。
搜索公衆號「頓悟源碼」獲取更多源碼分析和造的輪子。