LinkedHashMap是HashMap的子類,通過維護一個雙向鏈表,實現Map有序遍歷元素的特性。
因此,對於LinkedHashMap來說,其基本特性如下:
基本特性 | 結論 |
---|---|
元素是否允許為null | key和value可以為null |
元素是否允許重復 | key重復會覆蓋,value可以重復 |
是否有序 | 有 |
是否線程安全 | 否 |
源碼分析
本文使用的是JDK 1.8.0_201的源碼。
成員變量
LinkedHashMap是繼承HashMap的成員變量,實現有序的特性,還維護了額外幾個成員變量:
成員變量 | 作用 |
---|---|
transient LinkedHashMap.Entry<K,V> head; | 鏈表的頭部 |
transient LinkedHashMap.Entry<K,V> tail; | 鏈表的尾部 |
final boolean accessOrder; | 遍歷的模式,默認是false |
put操作
LinkedHashMap沒有實現自己的put操作,繼承自HashMap。問題在於,LinkedHashMap是怎么維護元素的插入順序的呢?換句話說,LinkedHashMap的雙向鏈表是在哪里維護的?答案是在HashMap put方法中調用的模板方法newNode()。HashMap中存在許多的模板方法,以方便LinkedHashMap去實現自己的特性。
對於LinkedHashMap來說,newNode()方法實現如下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 創建一個Entry
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;
}
}
get操作
LinkedHashMap自己實現了get()方法,代碼如下:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
// 與HashMap的區別在這里
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
上面的代碼與HashMap的區別在於accessOrder條件的判斷,如果accessOrder為true,程序就會去執行afterNodeAccess(e)方法。
LRU緩存實現
accessOrder變量,是LinkedHashMap中一個有趣的屬性。我們先看源碼文檔注釋:
A structural modification is any operation that adds or deletes one or more mappings or, in the case of access-ordered linked hash maps, affects iteration order. In insertion-ordered linked hash maps, merely changing the value associated with a key that is already contained in the map is not a structural modification. In access-ordered linked hash maps, merely querying the map with get is a structural modification.
對於insertion-ordered模式來說,即accessOrder為false的情況,只有添加和刪除操作,才會改變元素的順序。而對於access-ordered模式來說,即accessOrder為true的情況,即使是get()操作,都會改變元素的順序。
access-ordered模式,是實現LRU緩存(最近最少使用)的關鍵。所謂最近最少使用,就是說當緩存滿了,優先淘汰那些最近最少被訪問的數據。
LinkedHashMap的具體實現就是,每次訪問時,都把訪問的數據移到雙向隊列的尾部,那么隊列頭部的元素就是最少被訪問的數據,每次要淘汰數據時,就刪除隊列頭部的數據。
回過頭再看前面get操作中的afterNodeAccess(e)方法,就會發現這個方法正是每次訪問時,把訪問數據移到隊尾的關鍵。
總結
1. LinkedHashMap是有序的嗎?它是如何保證有序的?
LinkedHashMap通過維護一個雙向鏈表,從而保證元素的順序。需要注意的是,LinkedHashMap保證的有序是指元素的插入順序或者訪問順序,而不是指元素字典順序。至於LinkedHashMap具體保證的是插入順序還是訪問順序,通過初始化時accessOrder字段的值來確定。如果accessOrder=true,表示訪問順序,false表示插入順序。
2. 如何使用LinkedHashMap實現一個LRU緩存?
LinkedHashMap有兩種維護順序的模式,insetion-ordered模式跟access-ordered模式,當成員變量accessOrder為true,即access-ordered模式時,每次訪問元素時,就會將元素移到隊列的尾部,這樣頭部的元素就成了最少被訪問的元素。當需要淘汰數據時,頭部的元素先被淘汰。這個特性非常適合用來實現LRU緩存。
至於何時淘汰數據,LinkedHashMap提供了一個模板方法removeEldestEntry()。具體做法如下:
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int size;
public LRUCache(int size) {
super(size, 0.75f, true);
this.size = size;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當緩存的元素個數大於設置的大小時,淘汰最老的那個元素
return this.size() > this.size;
}
}
3. 為什么LinkedHashMap的遍歷性能會比HashMap好?
因為LinkedHashMap的遍歷是通過雙向鏈表實現的,與size的大小有關,即與map的實際元素個數有關。與此同時,HashMap的遍歷,是需要遍歷底層數組的,舉個例子,一個只存放了一個元素的HashMap,其底層數組的大小至少是16,那么這個遍歷有15次是多余的。由於負載因子的存在(不考慮負載因子大於0的情況),HashMap的遍歷總是會存在多余的次數。