1. 程式人生 > >淺析linkedHashMap雙向連結串列與Lru演算法的關係

淺析linkedHashMap雙向連結串列與Lru演算法的關係

 LinkedHashMap繼承HashMap,與HashMap的不同主要是它重寫了父類的Entry這個類,以及addEntry(),recordAccess()等方法。
  1. HashMap是單向的連結串列,因為他的儲存物件Entry中只有儲存Entry next,迭代輸出時按預設的插入順序
  2. linkedHashMap是雙向連結串列,因為他重寫了父類Entry增加了Entry before以及Entry after,迭代輸出時可以按插入順序,也可以按訪問順序
Entry:是連結串列儲存的實體物件,其中包含了key,value,hash,entry等。Entry物件的儲存是最終以陣列table[]的形式儲存在該陣列中,並且根據相關的hash值可以確定每          個Entry對應的陣列下標index。      分析linkedHashMap要從put方法開始,linkedHsapMap的put方法直接使用的是父類HashMap的put方法,
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;
}
繼續分析,呼叫的是addEntry的方法,此時需要注意子類LinkedHashMap重寫了該方法 這裡注意下父類的構造方法中有個inti();方法,該方法並未具體實現邏輯而是交給子類去重寫,linkedHashMap的init()方法初始化了一個Header,實際就是實體Entry,並且他的before和after都是自己。
@Override
void init() {
    header = new LinkedHashMapEntry<>(-1, null, null, null);
    header.before = header.after = header;
}
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);  //呼叫父類的建立方法
}
接下來父類的addEntry會呼叫子類的creatEntry的方法:
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;
}
將被移除的資料保留在連結串列頭部,也就是會移除最老的資料!