我的jdk源碼(十六):LinkedHashMap類

一、概述

    LinkedHashMap類是繼承自HashMap類,但是在HashMap的數據結構基礎上,使得每個桶的元素又通過新Entry特殊的結構,組成一條雙向鏈表。有了雙向鏈表的結構,就能保證LinkedHashMap的實例在默認情況下能夠保持元素的插入順序。

二、源碼剖析

    (1) 類的聲明

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

    LinkedHashMap的繼承實現結構比較簡單,就是繼承了HashMap類,然後實現了Map類,讓LinkedHashMap擁有Map的特性。

    (2) 元素結構

    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);
        }
    }

    LinkedHashMap類的結點Entry類實際是繼承了HashMap類的結點Node類,並且在此基礎上添加了before和after兩個引用,用來記錄雙向鏈表的前後結點。

    (3) 成員變量

    //序列化標識ID
    private static final long serialVersionUID = 3801124242820219131L;
    //記錄頭結點
    transient LinkedHashMap.Entry<K,V> head;
    //記錄尾結點
    transient LinkedHashMap.Entry<K,V> tail;
    //當accessOrder設置爲false時,會按照插入順序進行排序(在創建新節點的時候,把該節點放到了尾部),當accessOrder爲true時,會按照訪問順序(也就是插入和訪問都會將當前節點放置到尾部,尾部代表的是最近訪問的數據)
    final boolean accessOrder;

    LinkedHashMap類中除了記錄了頭尾結點外,最重要的設置屬性accessOrder來維持LinkedHashMap的實例中元素的順序,有兩種情況:當accessOrder設置爲false時,會按照插入順序進行排序(在創建新節點的時候,把該節點放到了尾部);當accessOrder爲true時,會按照訪問順序(也就是插入和訪問都會將當前節點放置到尾部,尾部代表的是最近訪問的數據)。

    對於成員變量accessOrder來說,使用final關鍵字修飾,表示不能改變:

    a、和局部變量的不同點在於,成員變量有默認值,因此必須手動賦值

    b、final的成員變量可以定義的時候直接賦值,或者使用構造方法在構造方法體裏面賦值,但是隻能二者選其一

    c、如果沒有直接賦值,那就必須保證所有重載的構造方法最終都會對final的成員變量進行了賦值

    (4) 構造方法

    //無參構造函數
    public LinkedHashMap() {
        //調用父類HashMap的構造函數(下同)
        super();
        //設置默認的排序規則爲插入順序(下同)
        accessOrder = false;
    }

    //帶初始容量的構造函數,initialCapacity也只是建議容量,並非最終容量
    public LinkedHashMap(int initialCapacity) {
        //調用父類HashMap的構造函數
        super(initialCapacity);
        //設置默認的排序規則爲插入順序
        accessOrder = false;
    }

    //帶初始容量和負載因子的構造函數
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    //帶初始容量、負載因子、排序規則的構造函數
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

    //參數爲Map的構造函數
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        //將原Map中的元素插入新LinkedHashMap中
        putMapEntries(m, false);
    }

    可以看到LinkedHashMap類幾乎所有的構造函數都是調用的父類HashMap的構造函數,只是多了一步設置成員變量accessOrder爲false的操作。當LinkedHashMap()是帶Map的構造函數的時候,就需要調用HashMap的putMapEntries()方法,使得原Map的元素變得有序了,並且順序就爲元素插入順序。

    (5) putMapEntries()方法

    //將一個Map中的所有元素添加到LinkedHashMap中,排序規則爲evict
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        //獲取m的元素個數,必須大於0才執行
        int s = m.size();
        if (s > 0) {
            如果LinkedHashMap中的容器table爲null 
            if (table == null) { // pre-size
                //獲取根據m的元素個數s獲取table的新容量,此時是float類型ft 
                float ft = ((float)s / loadFactor) + 1.0F;
                //判斷新容量是否小於2的30次方MAXIMUM_CAPACITY,小於則取ft的整數部分爲t,大於則取MAXIMUM_CAPACITY
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果新容量t是否大於了閾值threshold,如果大於了,那麼設置閾值threshold爲大於等於形容量t的最小2次冪
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            //如果容器table不爲null,那麼先判斷m中的元素個數s是否超過了閾值,超過則調用HashMap的resize()方法進行擴容
            else if (s > threshold)
                resize();
            //容器準備就緒,就開始往LinkedHashMap循環放入元素
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                //調用HashMap的putVal()方法添加元素
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

    resize()方法涉及HashMap的動態擴容,是HashMap的核心方法之一,就不再多說,詳細請看《我的jdk源碼(十三):HashMap  一磕到底,追根溯源!》。putVal()方法雖然是寫在HashMap類裏,但是裏面調用的afterNodeAccess()方法,HashMap中並沒具體實現,而是在LinkedHashMap中重寫了該方法,並且LinkedHashMap還重寫了newNode()方法,那麼我們結合putVal()方法和afterNodeAccess()方法一起看一下。

    (6) putVal()方法和LinkedHashMap.afterNodeAccess()等方法

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 判斷當前桶是否爲空,空的就需要初始化(resize 中會判斷是否進行初始化)。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 根據當前 key 的 hashcode 定位到具體的桶中並判斷是否爲空,爲空表明沒有 Hash 衝突就直接在當前位置創建一個新桶即可。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已經存在元素
    else {
        Node<K,V> e; K k;
        // 如果當前桶有值( Hash 衝突),那麼就要比較當前桶中的key、key 的 hashcode與寫入的 key 是否相等,相等就賦值給e,在第下面會統一進行賦值及返回。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                // 將第一個元素賦值給e,用e來記錄
                e = p;
        // 如果當前桶爲紅黑樹,那就要按照紅黑樹的方式寫入數據。
        else if (p instanceof TreeNode)
            // 放入樹中
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 該鏈爲鏈表
        else {
            //如果是個鏈表,就需要將當前的 key、value 封裝成一個新節點寫入到當前桶的後面(形成鏈表)。
            for (int binCount = 0; ; ++binCount) {
                // 到達鏈表的尾部
                if ((e = p.next) == null) {
                    // 在尾部插入新結點
                    p.next = newNode(hash, key, value, null);
                    // 接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    // 跳出循環
                    break;
                }
                // 判斷鏈表中結點的key值與插入的元素的key值是否相等
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    // 相等,跳出循環
                    break;
                // 用於遍歷桶中的鏈表,與前面的e = p.next組合,可以遍歷鏈表
                p = e;
            }
        }
        // 接着判斷當前鏈表的大小是否大於預設的閾值,大於時就要轉換爲紅黑樹。
        if (e != null) { 
            // 記錄e的value
            V oldValue = e.value;
            // onlyIfAbsent爲false或者舊值爲null
            if (!onlyIfAbsent || oldValue == null)
                //用新值替換舊值
                e.value = value;
            // 調用afterNodeAccess()方法進行鏈表排序
            afterNodeAccess(e);
            // 返回舊值
            return oldValue;
        }
    }
    // 結構性修改
    ++modCount;
    // 最後判斷是否需要進行擴容。超過最大容量就擴容,實際大小大於閾值則擴容。
    if (++size > threshold)
        resize();
    // 調用afterNodeInsertion()方法進行鏈表排序
    afterNodeInsertion(evict);
    return null;
}

    //這是LinkedHashMap重寫後的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;
    }

    //把元素P放到雙向鏈表的最後
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

    //這是LinkedHashMap重寫後的afterNodeAccess()方法,當accessOrder爲true並且傳入的節點不是最後一個時,然後將該節點放到尾部
void afterNodeAccess(Node<K,V> e) {
    //在執行方法前的上一次的尾結點
    LinkedHashMap.Entry<K,V> last;
    //當accessOrder爲true並且傳入的節點並不是上一次的尾結點時,執行下面的方法
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //p:當前節點
        //b:當前節點的前一個節點
        //a:當前節點的後一個節點;
        
        //將p.after設置爲null,斷開了與後一個節點的關係,但還未確定其位置
        p.after = null;
        /**
         * 因爲將當前節點p拿掉了,那麼節點b和節點a之間斷開了,我們先站在節點b的角度建立與節點a
         * 的關聯,如果節點b爲null,表示當前節點p是頭結點,節點p拿掉後,p的下一個節點a就是頭節點了;
         * 否則將節點b的後一個節點設置爲節點a
         */
        if (b == null)
            head = a;
        else
            b.after = a;
        /**
         * 因爲將當前節點p拿掉了,那麼節點a和節點b之間斷開了,我們站在節點a的角度建立與節點b
         * 的關聯,如果節點a爲null,表示當前節點p爲尾結點,節點p拿掉後,p的前一個節點b爲尾結點,
         * 但是此時我們並沒有直接將節點p賦值給tail,而是給了一個局部變量last(即當前的最後一個節點),因爲
         * 直接賦值給tail與該方法最終的目標並不一致;如果節點a不爲null將節點a的前一個節點設置爲節點b
         *
         * (因爲前面已經判斷了(last = tail) != e,說明傳入的節點並不是尾結點,既然不是尾結點,那麼
         * e.after必然不爲null,那爲什麼這裏又判斷了a == null的情況?
         * 以我的理解,java可通過反射機制破壞封裝,因此如果都是反射創建出的Entry實體,可能不會滿足前面
         * 的判斷條件)
         */
        if (a != null)
            a.before = b;
        else
            last = b;
        /**
         * 正常情況下last應該也不爲空,爲什麼要判斷,原因和前面一樣
         * 前面設置了p.after爲null,此處再將其before值設置爲上一次的尾結點last,同時將上一次的尾結點
         * last設置爲本次p
         */
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        //最後節點p設置爲尾結點,完事
        tail = p;
        ++modCount;
    }
}


    //這是LinkedHashMap重寫後的afterNodeInsertion()方法,目的是移除鏈表中最老的節點對象,也就是當前在頭部的節點對象,但實際上在JDK8中不會執行,因爲removeEldestEntry方法始終返回false。
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        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;
    }

    由源碼可以得知,當accessOrder設置爲false時,會按照插入順序進行排序,當accessOrder爲true時,會按照訪問順序進行排序。具體的操作就是,把元素放到雙向鏈表的末尾。

    (7) get()方法

    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;
    }

三、總結

    LinkedHashMap類其實也比較好理解,動態擴容就是HashMap的resize()方法,LinkedHashMap類只是會根據屬性accessOrder值來進行排序,當accessOrder爲默認值false的時候,每次插入元素的時候,就將插入的元素放到雙向鏈表的末尾;當accessOrder爲true時,每次調用put方法和get方法的時候,都會將元素放到鏈表的末尾。敬請期待《 我的jdk源碼(十七):Objects 》。

    更多精彩內容,敬請掃描下方二維碼,關注我的微信公衆號【Java覺淺】,獲取第一時間更新哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章