Java源碼解析之LinkedHashMap
HashMap 是用於映射(鍵值對)處理的數據類型,不保證元素的順序按照插入順序來排列,爲了解決這一問題,Java 在 JDK1.4 以後提供了 LinkedHashMap 來實現有序的 HashMap
LinkedHashMap 是 HashMap 的子類,它保留了元素的插入順序,在內部維護着一個按照元素插入順序或者元素訪問順序來排列的鏈表,默認是按照元素的插入順序來排列,就像使用 ArrayList 一樣;如果是按照元素的訪問順序來排列,則訪問元素後該元素將移至鏈表的尾部,可以以此來實現 LRUcache 緩存算法
一、結點類
前面說了,LinkedHashMap 是 HashMap 的子類,即 LinkedHashMap 的主要數據結構實現還是依靠 HashMap 來實現,LinkedHashMap 只是對 HashMap 做的一層外部包裝,這個從 LinkedHashMap 內聲明的結點類就可以看出來
Entry 類在 Node 類的基礎上擴展了兩個新的成員變量,這兩個成員變量就是 LinkedHashMap 來實現有序訪問的關鍵,不管結點對象在 HashMap 內部爲了解決哈希衝突採用的是鏈表還是紅黑樹,這兩個變量的指向都不受數據結構的變化而影響
從這也可以看出集合框架在設計時一個很巧妙的地方:LinkedHashMap 內部沒有新建一個鏈表用來維護元素的插入順序,而是通過擴展父類來實現自身的功能
//LinkedHashMap 擴展了 HashMap.Node 類
//在其基礎上新增了兩個成員變量用於指定上一個結點 before 和下一個結點 after
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);
}
}
二、成員變量
變量 accessOrder 用於決定 LinkedHashMap 中元素的排序方式,變量 tail 則用於幫助當 accessOrder 爲 true 時最新使用的一個結點的指向
//序列化ID
private static final long serialVersionUID = 3801124242820219131L;
//指向雙向鏈表的頭結點
transient LinkedHashMap.Entry<K,V> head;
//指向最新插入的一個結點
transient LinkedHashMap.Entry<K,V> tail;
//如果爲true,則內部元素按照訪問順序排序
//如果爲false,則內部元素按照插入順序排序
final boolean accessOrder;
三、構造函數
//自定義初始容量與裝載因子
//內部元素按照插入順序進行排序
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//自定義裝載因子
//內部元素按照插入順序進行排序
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//使用默認的初始容量以及裝載因子
//內部元素按照插入順序進行排序
public LinkedHashMap() {
super();
accessOrder = false;
}
//使用初始數據
//內部元素按照插入順序進行排序
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/**
* @param initialCapacity 初始容量
* @param loadFactor 裝載因子
* @param accessOrder 如果爲true,則內部元素按照訪問順序排序;如果爲false,則內部元素按照插入順序排序
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
四、插入元素
在 HashMap 中有三個空實現的函數,源碼註釋中也寫明這三個函數是準備由 LinkedHashMap 來實現的
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
當中,如果在調用 put(K key, V value)
方法插入元素時覆蓋了原有值,則afterNodeAccess
方法會被調用,該方法用於將最新訪問的鍵值對移至鏈表的尾部,其在 LinkedHashMap 的實現如下所示
//當訪問了結點 e 時調用
//結點 e 是最新訪問的一個結點,此處將結點 e 置爲鏈表的尾結點
void afterNodeAccess(Node<K,V> e) {
//last 用來指向鏈表的尾結點
LinkedHashMap.Entry<K,V> last;
//只有當上一次訪問的結點不是結點 e 時((last = tail) != e),才需要進行下一步操作
if (accessOrder && (last = tail) != e) {
//p 是最新訪問的一個結點,b 是結點 p 的上一個結點,a 是結點 p 的下一個結點
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//因爲結點 p 將成爲尾結點,所以 after 置爲null
p.after = null;
//如果 b == null ,說明結點 p 是原鏈表的頭結點,則此時將 head 指向下一個結點 a
//如果 b != null ,則移除結點 b 對結點 p 的引用
if (b == null)
head = a;
else
b.after = a;
//如果 a !=null,說明結點 p 不是原鏈表的尾結點,則移除結點 a 對結點 p 的引用
//如果 a == null,則說明結點 p 是原鏈表的尾結點,則讓 last 指向結點 b
if (a != null)
a.before = b;
else
last = b;
//如果 last == null,說明原鏈表爲空,則此時頭結點就是結點 p
//如果 last != null,則建立 last 和實際尾結點 p 之間的引用
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
//最新一個引用到的結點就是 tail
tail = p;
++modCount;
}
}
此外,當 put 方法調用結束時,afterNodeInsertion
方法會被調用,用於判斷是否移除最近最少使用的元素,依此可以來構建 LRUcache 緩存
//在插入元素後調用,此方法可用於 LRUcache 算法中移除最近最少使用的元素
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//如果在構造函數中參數 accessOrder 傳入了 true ,則鏈表將按照訪問順序來排列
//即最新訪問的結點將處於鏈表的尾部,依此可以來構建 LRUcache 緩存
//此方法就用於決定是否移除最舊的緩存,默認返回 false
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
五、訪問元素
在訪問元素時,如果 accessOrder 爲 true ,則會將訪問的元素移至鏈表的尾部,由於鏈表內結點位置的改變僅僅是修改幾個引用即可,所以這個操作還是非常輕量級的
//獲取鍵值爲 key 的鍵值對的 value
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;
}
//獲取鍵值爲 key 的鍵值對的 value,如果 key 不存在,則返回默認值 defaultValue
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
六、移除元素
當 HashMap 內部移除了某個結點時,LinkedHashMap 也要移除維護的鏈表中對該結點的引用,對應的是以下方法
//在移除結點 e 後調用
void afterNodeRemoval(Node<K,V> e) {
//結點 b 指向結點 e 的上一個結點,結點 a 指向結點 e 的下一個結點
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//移除結點 p 對相鄰結點的引用
p.before = p.after = null;
//如果 b == null,說明結點 p 是原鏈表的頭結點,則移除結點 p 後新的頭結點是 a
//如果 b != null,則更新結點間的引用
if (b == null)
head = a;
else
b.after = a;
//如果 a == null,說明結點 a 是尾結點,則移除結點 p 後最新一個訪問的結點就是原倒數第二的結點
//如果 a != null,則更新結點間的引用
if (a == null)
tail = b;
else
a.before = b;
}
七、LRUCache
在 Android 的實際應用開發中,LRUCache 算法是很常見的,一種典型的用途就是用來在內存中緩存 Bitmap,因爲從 IO 流中讀取 Bitmap 的資源消耗較大,爲了防止多次從磁盤中讀取某張圖片,所以可以選擇在內存中緩存 Bitmap。但內存空間也是有限的,所以也不能每張圖片都進行緩存,需要有選擇性地緩存一定數量的圖片,而最近最少使用算法(LRUCache)是一個可行的選擇
這裏利用 LinkedHashMap 可以按照元素使用順序進行排列的特點,來實現一個 LRUCache 策略的緩存
public class LRUCache {
private static class LRUCacheMap<K, V> extends LinkedHashMap<K, V> {
//最大的緩存數量
private final int maxCacheSize;
public LRUCacheMap(int maxCacheSize) {
super(16, 0.75F, true);
this.maxCacheSize = maxCacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxCacheSize;
}
}
public static void main(String[] args) {
//最大的緩存數量
final int maxCacheSize = 5;
LRUCacheMap<String, Integer> map = new LRUCacheMap<>(maxCacheSize);
for (int i = 0; i < maxCacheSize; i++) {
map.put("leavesC_" + i, i);
}
//輸出結果是:leavesC_0 leavesC_1 leavesC_2 leavesC_3 leavesC_4
System.out.println();
Set<String> keySet = map.keySet();
keySet.forEach(key -> System.out.print(key + " "));
//獲取鏈表的頭結點的值,使之移動到鏈表尾部
map.get("leavesC_0");
System.out.println();
keySet = map.keySet();
//輸出結果是://leavesC_1 leavesC_2 leavesC_3 leavesC_4 leavesC_0
keySet.forEach(key -> System.out.print(key + " "));
//向鏈表添加元素,使用達到緩存的最大數量
map.put("leavesC_5", 5);
System.out.println();
//輸出結果是://leavesC_2 leavesC_3 leavesC_4 leavesC_0 leavesC_5
//最近最少使用的元素 leavesC_1 被移除了
keySet.forEach(key -> System.out.print(key + " "));
}
}