LinkedHashMap源碼解析——基於JDK1.8

前言

LinkedHashMap繼承自HashMap。和HashMap不同的是,它維護一條雙向鏈表,解決了遍歷順序和插入順序一致的問題。並且還對訪問順序提供了相應的支持。因爲LinkedHashMap的很多實現是基於HashMap實現的,所以如果要讀懂LinkedHashMap還是需要先了解HashMap。可以參考我的這篇文章

    /**
     * 指向雙向鏈表的頭結點(最老的結點)
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 指向雙向鏈表的尾結點(最年輕的結點)
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * LinkedHashMap的迭代排序方法,如果是true,是進入順序,如果爲false,爲插入順序
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;

1 常量介紹

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the default initial capacity (16) and load factor (0.75).
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and a default load factor (0.75).
     *
     * @param  initialCapacity the initial capacity
     * @throws IllegalArgumentException if the initial capacity is negative
     */
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    /**
     * Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
     * with the specified initial capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

2 鏈表結點的插入過程(LinkedHashMap保持順序的原理)

首先是LinkedHashMap的插入過程。使用put方法,LinkedHashMap本身並沒有重寫自己繼承的put方法,它用的還是HashMap中的put方法,那究竟是怎麼做到不重寫put方法就保證了插入的順序了呢?

LinkedHashMap.Entry

    /**
     * HashMap.Node的子類
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

原本HashMap的Node結點肯定不會記錄插入順序,但是上面的Entry結點就不一樣了,它可以記錄自己的上一個結點和下一個結點,記錄插入的順序。那這個結點是怎麼添加進去的呢?那就不得不說另一個重要的方法了。LinkedHashMap通過重寫HashMap的newNode方法,在HashMap添加結點時調用了子類LinkedHashMap的newNode方法,實現Entry結點的插入。這樣既添加了雙向鏈表的需求又沒有破壞原本HashMap原本的方法。

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

LinkedHashMap#linkNodeLast

    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //將尾結點複製給last
        LinkedHashMap.Entry<K,V> last = tail;
        //新添加的結點爲尾結點
        tail = p;
        //如果是第一次添加,尾結點也是頭結點
        if (last == null)
            head = p;
        //否則
        else {
            //將上一個尾結點賦值給新添加結點的上一個結點,插入結點的前驅
            p.before = last;
            //上一個尾結點的後繼是當前尾結點
            last.after = p;
        }
    }

在HashMap中結點的類型有兩種,一種是Node普通結點,一種是TreeNode樹節點,Entry繼承了HashMap.Node,可以無縫的加入,那麼TreeNode呢?我們來看看HashMap中TreeNode結點的定義。

HashMap.TreeNode

    /**
     * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
     * extends Node) so can be used as extension of either regular or
     * linked node.
     */
    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
        ....
    }

我們可以看到,HashMap中的TreeNode結點居然繼承了LinkedHashMap.Entry結點,因爲這個緣故,當然可以直接在LinkedHashMap中使用TreeNode了。

LinkedHashMap#newTreeNode

    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

補充

當使用HashMap的putVal方法時還會調用一個在LinkedHashMap中重要的操作afterNodeInsertion

HashMap#putVal

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        ....
        ++modCount;
        if (++size > threshold)
            resize();
        //注意這個操作
        afterNodeInsertion(evict);
        return null;
    }

LinkedHashMap#afterNodeInsertion

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

LinkedHashMap#removeEldestEntry

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

當我們基於 LinkedHashMap 實現緩存時,通過覆寫removeEldestEntry方法可以實現自定義策略的 LRU 緩存。

3 鏈表結點的刪除過程

LinkedHashMap結點的刪除操作同樣的是使用父類HashMap內的相關方法(remove,removeNode),而沒有重寫相關方法。那該方法又怎麼保持刪除結點後鏈表的維護呢?讓我們來看相關代碼。

HashMap#remove

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

HashMap#removeNode

   final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //p表示頭結點或者將要被刪除的結點的上一個結點
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        //如果table不爲空,長度>0,table中hash對應下標的第一個結點不爲null,有值
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            //node指將要被刪除的結點
            Node<K,V> node = null, e; K k; V v;
            //與上面相同
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            //鏈表頭結點不等,且後面還有結點
            else if ((e = p.next) != null) {
                //樹節點
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            //賦值給node
                            node = e;
                            break;
                        }
                        //賦值給p,方便後面操作
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                //如果是樹節點
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                //如果頭結點是將要被刪除的結點
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                //刪除結點後維護鏈表的關鍵操作!!!!!!
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

刪除結點後關鍵的操作是afterNodeRemoval方法,對於HashMap來說,該方法未做任何操作。LinkedHashMap重寫了該方法,實現了刪除結點後鏈表的維護操作。

LinkedHashMap#afterNodeRemoval

    void afterNodeRemoval(Node<K,V> e) { // unlink
        //複製刪除結點e,並記錄刪除結點的上一個結點和下一個結點
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //將刪除結點指向前結點和後結點的引用清空
        p.before = p.after = null;
        //如果刪除結點的前一個結點爲空,表明是第一個結點,改變head指向刪除結點的下一個結點
        if (b == null)
            head = a;
        //否則前面存在結點,(刪除節點的上一個結點的after)指向(刪除結點的下一個結點)
        else
            b.after = a;
        //如果刪除結點爲尾結點,改變tail指向刪除結點的上一個結點
        if (a == null)
            tail = b;
        //否則改變(刪除結點的下一個結點的before)指向(刪除結點的上一個結點)
        else
            a.before = b;
    }

4 鏈表結點的獲得過程

LinkedHashMap#get

    /**
     * 返回指定鍵的值
     * Returns the value to which the specified key is mapped,
     * or {@code null} if this map contains no mapping for the key.
     * 如果map中保存着相應的鍵,返回相應的值,否則返回null
     * <p>More formally, if this map contains a mapping from a key
     * {@code k} to a value {@code v} such that {@code (key==null ? k==null :
     * key.equals(k))}, then this method returns {@code v}; otherwise
     * it returns {@code null}.  (There can be at most one such mapping.)
     * //返回null不一定代表不存在鍵
     * <p>A return value of {@code null} does not <i>necessarily</i>
     * indicate that the map contains no mapping for the key; it's also
     * possible that the map explicitly maps the key to {@code null}.
     * The {@link #containsKey containsKey} operation may be used to
     * distinguish these two cases.
     */
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //如果爲true,將結點移動到鏈表末尾
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

LinkedHashMap#afterNodeAccess

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        //accessOrder=true並且訪問的結點不是尾結點
        if (accessOrder && (last = tail) != e) {
            //複製訪問結點e,並記錄刪除結點的上一個結點和下一個結點
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //訪問節點的after置空(變成尾結點)
            p.after = null;
            //如果當前結點e爲頭結點,更換頭結點爲下一個結點
            if (b == null)
                head = a;
            //否則,當前結點的上一個結點的after指向當前結點的下一個結點
            else
                b.after = a;
            //如果當前結點不是尾結點,當前結點的下一個結點的before指向當前節點的上一個結點
            if (a != null)
                a.before = b;
            //否則,last指向當前結點的上一個結點(這裏是爲了適配非尾結點的狀況,p.before = last)
            else
                last = b;
            //如果既是頭結點又是尾結點,頭結點指向當前結點(上面頭結點指向空)
            if (last == null)
                head = p;
            //否則
            else {
                //當前結點的上一個結點是尾結點
                p.before = last;
                //尾結點的下一個結點是當前結點(至此將訪問的當前結點移動到鏈表尾部)
                last.after = p;
            }
            //尾結點指向當前結點
            tail = p;
            ++modCount;
        }
    }

測試

Map<String, String> linkedHashMap = new LinkedHashMap<>(4, 0.75f, true);
        linkedHashMap.put("test1", "fjx1");
        linkedHashMap.put("test2", "fjx2");
        linkedHashMap.put("test3", "fjx3");
        System.out.println("開始時順序:");
        Set<Map.Entry<String, String>> set = linkedHashMap.entrySet();
        set.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));



 System.out.println("通過get方法,導致key爲test1對應的Entry到表尾");
        linkedHashMap.get("test1");
        Set<Map.Entry<String, String>> set2 = linkedHashMap.entrySet();
        set2.forEach((entry)-> System.out.println("key:" + entry.getKey() + ",value:" + entry.getValue()));    

發佈了36 篇原創文章 · 獲贊 8 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章