我們已經講解了HashMap的實現原理,LinkedHashMap是HashMap的子類,在其基礎上實現了元素的排序功能,接下來對LinkedHashMap的實現進行一個瞭解。
目錄
-
底層數據結構
LinkedHashMap就是實現了LRU(Least Recent Use, 最近最少使用)排序的HashMap,LinkedHashMap就是在HashMap的基礎上,在Entry<key,value>對象中添加了before和after兩個指針,構成一個雙向鏈表(最少使用的在head,最多使用的在tail),用來存儲元素的訪問順序(get和put都算訪問),這個雙向鏈表的重要操作就是,當訪問了一個元素,那麼把他從雙向鏈表中刪除,並把它放進雙向鏈表的表頭。
LinkedHashMap 很多方法直接繼承自 HashMap,僅爲維護雙向鏈表覆寫了部分方法。
LinkedHashMap的節點定義如下:
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);
}
}
我了便於理解,我們對LinkedHashMap做如下操作:
插入a——>插入b——>插入c——>訪問a
那麼我們可以得到按照訪問順序來說,排序應該是:b、c、a
此時的LinkedHashMap的結構如下:
LinkedHashMap定義了排序模式accessOrder,該屬性爲boolean型變量,對於訪問順序,爲true;對於插入順序,則爲false。
LinkedHashMap的處理思路如下:除了像HashMap一樣插入和查詢節點,還需要維護循環雙向鏈表,accessOrder爲true則雙向鏈表維護訪問順序,我們認爲get和put操作都是訪問操作,也就是說這兩個操作需要對雙向鏈表進行操作。那麼是如何對雙向鏈表進行處理的呢?當有相關訪問操作時,在雙向鏈表中刪掉當前節點,並將當前節點添加到雙向鏈表的尾部。
-
查詢操作get()
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)//getNode直接繼承父類的getNode方法
return null;
if (accessOrder)//按訪問順序排序
afterNodeAccess(e);//對雙向鏈表進行操作,將節點e移到雙向鏈表的尾部
return e.value;
}
那麼 afterNodeAccess(Node<K,V> e)是怎麼實現的呢?以下代碼和註釋進行了說明:
void afterNodeAccess(Node<K,V> e) { // move node to last即把最常用的放到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;//因爲要把p放到最後,因此p.after是null
if (b == null)//如果p在head(最前面),那麼去掉p以後p.after變成head
head = a;
else
b.after = a;//否則b直接鏈接a
if (a != null)
a.before = b;//如果p在鏈表中間,那麼去掉p以後b直接鏈接a
else
last = b;//如果p已經在最後,那麼去掉p以後last變成b
//以上代碼是刪掉p,接下來開始把p插到鏈表尾部
if (last == null)
head = p;//如果鏈表中只有p一個節點,那麼刪掉p以後則沒有節點,此時last==null,重新插入p節點爲head
else {//否則將p放在鏈表尾部即可
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
-
插入操作put()/putAll()
LinkedHashMap並沒有重寫HashMap的put()方法,那麼他是如何處理雙向鏈表的呢?
通過HashMap的put方法可以看出,其中又兩個方法afterNodeAccess(e)和afterNodeInsertion(evict),這兩個方法在HashMap中是空方法,在LinkedHashMap中被重寫(afterNodeAccess方法在上方)。
afterNodeInsertion(evict)方法的作用:如果removeEldestEntry返回true,表示map的容量一定,在插入一個節點的時候,如果map滿了,就需要刪掉最少訪問的一個節點(first),通過重寫removeEldestEntry
方法可以實現自定義的 LRU 緩存。
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;
}
afterNodeAccess(e)方法的作用是:調整雙向鏈表的節點順序。
但是有一個問題就是,put方法並沒有重寫,那麼插入的節點是HashMap的節點還是LinkedHashMap的節點呢?可以發現,在HashMap的put()方法中調用newNode()方法來構造節點,而LinkedHashMap重寫了改方法,因此在LinkedHashMap中調用put()方法時構造的是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);//將當前node添加到雙向鏈表的尾部
return p;
}
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)//如果last==null說明鏈表爲空,那麼直接降p放到head
head = p;
else {
p.before = last;
last.after = p;
}
}
-
刪除remove()方法
同樣的,刪除方法也是繼承自HashMap, 只是重寫了afterNodeRemoval(node)方法以便實現雙向鏈表的節點刪除操作
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;
}