淺析linkedHashMap雙向連結串列與Lru演算法的關係
阿新 • • 發佈:2019-02-15
LinkedHashMap繼承HashMap,與HashMap的不同主要是它重寫了父類的Entry這個類,以及addEntry(),recordAccess()等方法。
- HashMap是單向的連結串列,因為他的儲存物件Entry中只有儲存Entry next,迭代輸出時按預設的插入順序
- linkedHashMap是雙向連結串列,因為他重寫了父類Entry增加了Entry before以及Entry after,迭代輸出時可以按插入順序,也可以按訪問順序
繼續分析,呼叫的是addEntry的方法,此時需要注意子類LinkedHashMap重寫了該方法 這裡注意下父類的構造方法中有個inti();方法,該方法並未具體實現邏輯而是交給子類去重寫,linkedHashMap的init()方法初始化了一個Header,實際就是實體Entry,並且他的before和after都是自己。public V put(K key, V value) { if (table == EMPTY_TABLE) { // 如果初始陣列為空,就初始化一個容量,並且例項化一個數組 table = new Entry[capacity]; inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = sun.misc.Hashing.singleWordWangJenkinsHash(key); int i = indexFor(hash, table.length);// 根據產生的相關Hash值確定Entry的下標 for (HashMapEntry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; // 此時返回舊的資料,初次put的時候 不進入,返回為null } } modCount++; addEntry(hash, key, value, i); return null; }
@Override void init() { header = new LinkedHashMapEntry<>(-1, null, null, null); header.before = header.after = header; }
接下來父類的addEntry會呼叫子類的creatEntry的方法:void addEntry(int hash, K key, V value, int bucketIndex) { // Previous Android releases called removeEldestEntry() before actually // inserting a value but after increasing the size. // The RI is documented to call it afterwards. // **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE **** // Remove eldest entry if instructed LinkedHashMapEntry<K,V> eldest = header.after; if (eldest != header) { //如果連結串列header後面有資料就進入 boolean removeEldest; size++; try { removeEldest = removeEldestEntry(eldest); //呼叫此方法判斷是否要刪除頭部資料 也就是最老的資料 } finally { size--; } if (removeEldest) { removeEntryForKey(eldest.key); } } super.addEntry(hash, key, value, bucketIndex); //呼叫父類的建立方法 }
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMapEntry<K,V> old = table[bucketIndex];
LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
程式碼中主要關鍵的還是addBefore()這個方法:
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
將初始化的header代入賦值,最終會形成雙向連結串列的關係:
linkedHashMap的主要一個作用是它具有HashMap沒有訪問順序,按訪問順序迭代,超出數量限制後移除最老的元素是Lru演算法的主要原理!
那是如何實現這個訪問順序呢?我們發現在第二次put(而不是初次put操作)或者get的時候,都會呼叫一個方法 e.recordAccess(this);
看原始碼,這個是Entry實體的方法,字面意思是重新將連結串列排序
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove(); // 首先執行Remove方法
addBefore(lm.header); // 再呼叫addbefore方法重新加到連結串列的尾部去,此時最新的資料將會保留在連結串列尾部!
}
}
remove方法的作用是將被訪問的該實體的before與after兩個實體連線起來
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
經過這麼處理之後,最老的資料也就停留在了連結串列頭部,最新的資料就在連結串列尾部!記住這層關係!
實現lru演算法是我們在每次put元素進入時,都會檢查一下removeEldestEntry ,如果返回為true就會觸發移除老資料的方法,如下:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
將被移除的資料保留在連結串列頭部,也就是會移除最老的資料!