天天看點

【JDK專題】——JDK資料結構——LinkedHashMap源碼剖析

LinkedHashMap——基本結構

很明顯,LinkedHashMap直接繼承了HashMap;

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
     /**
     * 多了一個頭指針和尾指針
     */
    transient LinkedHashMap.Entry<K,V> head;
    transient LinkedHashMap.Entry<K,V> tail;
    /**
      * 很明顯繼承了原有Entry的屬性,如果要實作按照插入和周遊順序一樣的需求
      * 不改變原有的屬性,添加兩個before、after
      * LinkedHashMap的node重名為Entry
      */
	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);
        }
    }
     /**
      *  重寫了newNode
      */
   Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
    ```java
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        /**
         * @hashMap隻有一個節點(last == null)
         * 直接操作頭等于新節點
         */
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        /**
         * @否則将末尾節點和新節點調整連結清單結構
         * 操作新節點的前一個節點是末尾節點(p是原末尾節點)
         * 操作舊末尾節點的後一個節點等新節點
         * 其實就是新節點放最後,形成雙向連結清單
         */
        else {
            p.before = last;//操作新節點的前一個節點是末尾節點
            last.after = p;//操作舊末尾節點的後一個節點等新節點
        }
    }
           

HashMap原有的方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ……
        /*
          針對于key是null的重寫了newNode
         */
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
            ……
                /*
                  在連結清單中找到了key相同的值進行替換時,激活通路模式的回調
                 */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
         /**
          激活LRU模式的回調
         */
        afterNodeInsertion(evict);
        return null;
    }
    /**
      * 這是一個生命周期方法,為了實作擴充
      * 原有的是空實作,LinkedHashMap重寫了這些,我們下面逐個講解這些方法
      */
    void afterNodeAccess(Node<K,V> p) { }//通路模式專用的連結清單調整
    void afterNodeInsertion(boolean evict) { }//LRU模式專用的連結清單調整
    void afterNodeRemoval(Node<K,V> p) { }//删除時的的連結清單調整
    
           

LinkedHashMap——周遊

public final void forEach(Consumer<? super K> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            /*
            	周遊起始條件是:e首先=head;
            	周遊終止條件是:e==null;
            	周遊自增條件是:e.after,選擇下一個;
             */
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e.key);
            if (modCount != mc)
                throw new ConcurrentModificationException();
}
           

通過上面的案例,我們明顯的看出在node資料結構中的before和after就是實作有序周遊的關鍵;那麼具體在插入節點的時候是如何實作的呢?

LinkedHashMap——删除原理

删除肯定要将斷層相連,是以怎麼實作的呢?

void afterNodeRemoval(Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;

        /**
         * 原節點頭尾變成空
         */
        p.before = p.after = null;

        /**
         * @原節點是頭節點(前繼節點是null)
         * 頭節點直接等于後繼
         * 否則前繼的後繼向後相連
         */
        if (b == null)
            head = a;
        else
            b.after = a;
        /**
         * @原節點是尾節點(後繼節點是null)
         * 否則前繼的後繼相連向前相連
         */
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

           

LinkedHashMap——通路模式原理

通路模式下,通路元素和插入都會影響連結清單的雙向連結清單順序的,這是怎麼設計出來的呢?(非通路模式,就是按照插入的時候的linkNodeLast決定順序即可)

【JDK專題】——JDK資料結構——LinkedHashMap源碼剖析
final boolean accessOrder;
     public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        /*
          當元素被通路到後就會進行一次重新的before和after編排
         */
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
            ……
               /*
                    在連結清單中找到了key相同的值進行替換時
                    進行一次重新的before和after編排
                */
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        return null;
    }
    /**
     * @滞末處理
     * 更改節點至尾部,顯然要處理之前節點把斷層補上
     */
    void (Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        /**
         * @條件開啟了accessOrder通路順序模式且如果舊節點不是末尾節點才進入滞末處理
         * last指向末尾節點
         */
        if (accessOrder && (last = tail) != e) {
            
            
            /**
             * e代表舊的剛被替換的節點,p是強轉後的e,相容hashMap的方法原型
             * b代表被替換的前繼,a代表被替換的後繼
             */
            LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;//即将被調整後最後,是以最後肯定是null但是指針已經在前面保留了
            /**
             * @去除舊節點連接配接兩個節點中間的斷層
             */
            /**
             * @(1)舊節點是頭節點(沒前繼,前繼是空)
             * 直接頭結點指向自己的後繼
             */
            if (b == null)
                head = a;
            else //否則舊節點的前繼節點向後連接配接舊節點的後繼節點
                b.after = a;
            /**
             * @(2)舊節點是中間節點(後有繼不是空)
             * 直接舊節點的後繼向前連接配接舊節點的前繼,剛好上面相反
             */
            if (a != null)
                a.before = b;
            else //否則曾經是最後一個節點,将last指向前繼,繼續處理
                last = b;
            /**
             * @(3)處理隻有一個節點時,last也是空(last指向前繼是空或者tail是空)
             */
            if (last == null)
                head = p;
            else {

                /**
                 * 這裡算是一個兜底政策,可能有兩個情況
                 * 自己是中間節點,last就是tali,做一下指針調整
                 * 自己是最後一個節點,last是節點的前繼節點,就是上面那個情況,曾經是最後一個節點,是以前繼就是倒數二個,做一下這兩個的指針調整
                 */
                p.before = last;
                last.after = p;
            }
            tail = p;//調整整體指針
            ++modCount;
        }
    }
     void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        /**
         * @此次實作LRU算法
         * 需要子類繼承LinkHashMap實作removeEldestEntry方法,該方法傳入頭結點以及某種邏輯決定
         * 是否移除頭節點,因為頭節點意味着經常不被通路
         */
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
           

LinkedHashMap——LRU算法實作

在插入的時候有這麼一個回調處理,

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ……
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

           

LinkedHashMap的LRU方案

需要繼承LinkedHashMap并實作removeEldestEntry方法的邏輯

也就是LinkedHashMap預設不支援LRU,removeEldestEntry預設傳回false

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        /**
         * @此次實作LRU算法
         * 需要子類繼承LinkHashMap實作removeEldestEntry方法,該方法傳入頭結點以及某種邏輯決定
         * 是否移除頭節點,因為頭節點意味着經常不被通路
         */
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
           

LinkedHashMap——總結

雙向連結清單:node被包成entry多了一個before和after;然後在整體多了兩個頭指針和尾指針;

普通模式:隻是在插入的時候更新尾部節點和新節點的指針關系

通路模式:除此以為get方法也會影響節點的順序調整,這一切都通過afterNodeAccess回調實作;afterNodeAccess方法還有在插入的時候替換舊元素的時候也會激活;

LRU模式:預設一直不實作,就是删除頭節點;需要繼承LinkedHashMap重寫LRU算法的實作決定true和false來實作整個

GodSchool 緻力于簡潔的知識工程,輸出高品質的知識産出,我們一起努力 部落客私人微信:supperlzf
【JDK專題】——JDK資料結構——LinkedHashMap源碼剖析

繼續閱讀