jdk原始碼分析之LinkedHashMap

王世暉發表於2016-06-02

基本原理

LinkedHashMap繼承自HashMap,因此具有HashMap的所有特性。在HashMap的基礎上,保持了key的插入順序或者訪問順序。實現方法就是用雙向迴圈連結串列將所有的entry連結起來,讀取資料的時候直接讀取此雙向迴圈連結串列的資料即可。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
    /**
     * The head of the doubly linked list.
     */
    private transient Entry<K,V> header;

    /**
     * The iteration ordering method for this linked hash map: 
     * true for access-order,false for insertion-order.
     */
    private final boolean accessOrder;

header是雙向迴圈連結串列的頭結點,不儲存資料,用於定位頭尾節點
header.after指向連結串列的第一個節點
header.before指向連結串列的最後一個節點
accessOrder是一個標誌位,true的話表示按照訪問順序訪問連結串列,可據此構建LRU快取,fasle的話表示按照插入順序訪問連結串列

雙向迴圈連結串列的Entry節點

LinkedHashMap的Entry繼承自HashMap的Entry

 private static class Entry<K,V> extends HashMap.Entry<K,V> 

並新增了指向前序節點的前序指標和指向後繼節點的後繼指標

        // These fields comprise the doubly linked list used for iteration.
        Entry<K,V> before, after;

因此LinkedHashMap的Entry節點具有三個指標域,next指標維護Hash桶中衝突key的連結串列,before和after維護雙向迴圈連結串列
為了維護雙向迴圈連結串列,Entry新增加了4個方法

刪除節點remove

        /**
         * Removes this entry from the linked list.
         */
        private void remove() {
            before.after = after;
            after.before = before;
        }

充分體現了連結串列這種資料結構刪除中間節點的方便之處,僅僅修改指標的指向即可。
如果要刪除當前節點,就把當前節點的前序節點的後繼指標指向當前節點的後繼節點,當前節點的後繼節點的前序指標指向當前節點的前序節點即可,這樣當前節點就從連結串列中脫離開了,斷開的節點也得到重新連結。

新增節點

        /**
         * Inserts this entry before the specified existing entry in the list.
         */
        private void addBefore(Entry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }

新增節點同樣是指標操作,非常高效方便

記錄訪問recordAccess

        /**
         * 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();
                addBefore(lm.header);
            }
        }

當LinkedHashMap的標誌位accessOrder為true時,標誌著要採用訪問順序訪問連結串列。

 remove();
 addBefore(lm.header);

最新訪問的節點先從原來的位置刪除,然後重現新增到連結串列的末尾,這樣最近最少訪問的節點就被挪到了連結串列的前端。

LinkedHashMap新增資料

LinkedHashMap本質上也是一個HashMap,在HashMap的基礎上新增所有entry構成雙向迴圈連結串列的功能。因此LinkedHashMap並沒有覆寫HashMap的put方法,只是覆寫了HashMap中put方法呼叫的addEntry方法。
先來回顧下HashMap的put方法夠幹了什麼

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<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;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

HashMap的put方法主要功能是計算對應key的桶的位置,遍歷桶中連結串列,找到對應key的entry修改原資料。遍歷連結串列結束沒有找到對應key的entry則呼叫addEntry方法將新新增的鍵值對entry新增到桶中entry連結串列的頭部。
而對於LinkedHashMap中的entry節點來說,要維持兩個連結串列,一個是桶中的next指標域連結的hask衝突的key構成的entry單連結串列,第二個就是維護所有entry構成的雙向迴圈連結串列
LinkedHashMap的addEntry方法如下所示

    void addEntry(int hash, K key, V value, int bucketIndex) {
        createEntry(hash, key, value, bucketIndex);

        // Remove eldest entry if instructed, else grow capacity if appropriate
        Entry<K,V> eldest = header.after;
        if (removeEldestEntry(eldest)) {
            removeEntryForKey(eldest.key);
        } else {
            if (size >= threshold)
                resize(2 * table.length);
        }
    }

先呼叫createEntry把新節點連結到兩條連結串列上,然後判斷是否要刪除最近最久未訪問的節點(也就是雙向迴圈連結串列的第一個節點),要刪除的話就不需要檢查是否需要擴容了,都則要檢查是否需要擴容。

    void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
        table[bucketIndex] = e;
        e.addBefore(header);
        size++;
    }

可見新節點被兩條連結串列都連結上了
第一條是單項非迴圈連結串列,桶中hash值衝突得key構成的entry連結串列

        HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
        table[bucketIndex] = e;

第二條是所有entry節點構成的雙向迴圈連結串列

        e.addBefore(header);

containsValue方法直接在雙向迴圈連結串列中查詢值

    public boolean containsValue(Object value) {
        // Overridden to take advantage of faster iterator
        if (value==null) {
            for (Entry e = header.after; e != header; e = e.after)
                if (e.value==null)
                    return true;
        } else {
            for (Entry e = header.after; e != header; e = e.after)
                if (value.equals(e.value))
                    return true;
        }
        return false;
    }

根據傳入引數value是否為null,對雙向迴圈連結串列進行遍歷查詢

相關文章