面試必備:LinkedHashMap原始碼解析(JDK8)

mcxtzhang發表於2017-08-20

想看我更多文章:【張旭童的部落格】blog.csdn.net/zxt0601
想來gayhub和我gaygayup:【mcxtzhang的Github主頁】github.com/mcxtzhang

1 概述

上文中,我們已經聊過了HashMap,本篇是基於上文的基礎之上。所以如果沒看過上文,請先閱讀面試必備:HashMap原始碼解析(JDK8)
本文將從幾個常用方法下手,來閱讀LinkedHashMap的原始碼。
按照從構造方法->常用API(增、刪、改、查)的順序來閱讀原始碼,並會講解閱讀方法中涉及的一些變數的意義。瞭解LinkedHashMap的特點、適用場景。

如果本文中有不正確的結論、說法,請大家提出和我討論,共同進步,謝謝。

2 概要

概括的說,LinkedHashMap 是一個關聯陣列、雜湊表,它是執行緒不安全的,允許key為null,value為null
它繼承自HashMap,實現了Map<K,V>介面。其內部還維護了一個雙向連結串列,在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。

預設情況,遍歷時的順序是按照插入節點的順序。這也是其與HashMap最大的區別。
也可以在構造時傳入accessOrder引數,使得其遍歷順序按照訪問的順序輸出。

因繼承自HashMap,所以HashMap上文分析的特點,除了輸出無序,其他LinkedHashMap都有,比如擴容的策略,雜湊桶長度一定是2的N次方等等。
LinkedHashMap在實現時,就是重寫override了幾個方法。以滿足其輸出序列有序的需求。

示例程式碼:

根據這段例項程式碼,先從現象看一下LinkedHashMap的特徵:
在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。

        Map<String, String> map = new LinkedHashMap<>();
        map.put("1", "a");
        map.put("2", "b");
        map.put("3", "c");
        map.put("4", "d");

        Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

        System.out.println("以下是accessOrder=true的情況:");

        map = new LinkedHashMap<String, String>(10, 0.75f, true);
        map.put("1", "a");
        map.put("2", "b");
        map.put("3", "c");
        map.put("4", "d");
        map.get("2");//2移動到了內部的連結串列末尾
        map.get("4");//4調整至末尾
        map.put("3", "e");//3調整至末尾
        map.put(null, null);//插入兩個新的節點 null
        map.put("5", null);//5
        iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }複製程式碼

輸出:

1=a
2=b
3=c
4=d
以下是accessOrder=true的情況:
1=a
2=b
4=d
3=e
null=null
5=null複製程式碼

3 節點

LinkedHashMap的節點Entry<K,V>繼承自HashMap.Node<K,V>,在其基礎上擴充套件了一下。改成了一個雙向連結串列

    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);
        }
    }複製程式碼

同時類裡有兩個成員變數head tail,分別指向內部雙向連結串列的表頭、表尾。

    //雙向連結串列的頭結點
    transient LinkedHashMap.Entry<K,V> head;

    //雙向連結串列的尾節點
    transient LinkedHashMap.Entry<K,V> tail;複製程式碼

4 建構函式

    //預設是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。
    //為true時,可以在這基礎之上構建一個LruCach
    final boolean accessOrder;

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    //指定初始化時的容量,
    public LinkedHashMap(int initialCapacity) {
        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中的所有資料到 本集合中。
        putMapEntries(m, false);
    }複製程式碼

小結:
建構函式和HashMap相比,就是增加了一個accessOrder引數。用於控制迭代時的節點順序。

5 增

LinkedHashMap並沒有重寫任何put方法。但是其重寫了構建新節點的newNode()方法.
newNode()會在HashMapputVal()方法裡被呼叫,putVal()方法會在批量插入資料putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入單個資料public V put(K key, V value)時被呼叫。

LinkedHashMap重寫了newNode(),在每次構建新節點時,通過linkNodeLast(p);新節點連結在內部雙向連結串列的尾部

    //在構建新節點時,構建的是`LinkedHashMap.Entry` 不再是`Node`.
    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;
    }
    //將新增的節點,連線在連結串列的尾部
    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;
        }
    }複製程式碼

以及HashMap專門預留給LinkedHashMapafterNodeAccess() afterNodeInsertion() afterNodeRemoval() 方法。

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }複製程式碼
    //回撥函式,新節點插入之後回撥 , 根據evict 和   判斷是否需要刪除最老插入的節點。如果實現LruCache會用到這個方法。
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //LinkedHashMap 預設返回false 則不刪除節點
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    //LinkedHashMap 預設返回false 則不刪除節點。 返回true 代表要刪除最早的節點。通常構建一個LruCache會在達到Cache的上限是返回true
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }複製程式碼

void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是構建LruCache需要的回撥,在LinkedHashMap裡可以忽略它們。

6 刪

LinkedHashMap也沒有重寫remove()方法,因為它的刪除邏輯和HashMap並無區別。
但它重寫了afterNodeRemoval()這個回撥方法。該方法會在Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法中回撥,removeNode()會在所有涉及到刪除節點的方法中被呼叫,上文分析過,是刪除節點操作的真正執行者。

    //在刪除節點e時,同步將e從雙向連結串列上刪除
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        //待刪除節點 p 的前置後置節點都置空
        p.before = p.after = null;
        //如果前置節點是null,則現在的頭結點應該是後置節點a
        if (b == null)
            head = a;
        else//否則將前置節點b的後置節點指向a
            b.after = a;
        //同理如果後置節點時null ,則尾節點應是b
        if (a == null)
            tail = b;
        else//否則更新後置節點a的前置節點為b
            a.before = b;
    }複製程式碼

7 查

LinkedHashMap重寫了get()和getOrDefault()方法:

    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;
    }
    public V getOrDefault(Object key, V defaultValue) {
       Node<K,V> e;
       if ((e = getNode(hash(key), key)) == null)
           return defaultValue;
       if (accessOrder)
           afterNodeAccess(e);
       return e.value;
   }複製程式碼

對比HashMap中的實現,LinkedHashMap只是增加了在成員變數(建構函式時賦值)accessOrder為true的情況下,要去回撥void afterNodeAccess(Node<K,V> e)函式。

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }複製程式碼

afterNodeAccess()函式中,會將當前被訪問到的節點e,移動至內部的雙向連結串列的尾部。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;//原尾節點
        //如果accessOrder 是true ,且原尾節點不等於e
        if (accessOrder && (last = tail) != e) {
            //節點e強轉成雙向連結串列節點p
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //p現在是尾節點, 後置節點一定是null
            p.after = null;
            //如果p的前置節點是null,則p以前是頭結點,所以更新現在的頭結點是p的後置節點a
            if (b == null)
                head = a;
            else//否則更新p的前直接點b的後置節點為 a
                b.after = a;
            //如果p的後置節點不是null,則更新後置節點a的前置節點為b
            if (a != null)
                a.before = b;
            else//如果原本p的後置節點是null,則p就是尾節點。 此時 更新last的引用為 p的前置節點b
                last = b;
            if (last == null) //原本尾節點是null  則,連結串列中就一個節點
                head = p;
            else {//否則 更新 當前節點p的前置節點為 原尾節點last, last的後置節點是p
                p.before = last;
                last.after = p;
            }
            //尾節點的引用賦值成p
            tail = p;
            //修改modCount。
            ++modCount;
        }
    }複製程式碼

值得注意的是,afterNodeAccess()函式中,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時,如果同時查詢訪問資料,也會導致fail-fast,因為迭代的順序已經改變。

7.2 containsValue

它重寫了該方法,相比HashMap的實現,更為高效

    public boolean containsValue(Object value) {
        //遍歷一遍連結串列,去比較有沒有value相等的節點,並返回
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }複製程式碼

對比HashMap,是用兩個for迴圈遍歷,相對低效。

    public boolean containsValue(Object value) {
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
            for (int i = 0; i < tab.length; ++i) {
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }複製程式碼

8 遍歷

重寫了entrySet()如下:

    public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        //返回LinkedEntrySet
        return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
    }
    final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
        public final Iterator<Map.Entry<K,V>> iterator() {
            return new LinkedEntryIterator();
        }
    }複製程式碼

最終的EntryIterator:

    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

    abstract class LinkedHashIterator {
        //下一個節點
        LinkedHashMap.Entry<K,V> next;
        //當前節點
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            //初始化時,next 為 LinkedHashMap內部維護的雙向連結串列的扁頭
            next = head;
            //記錄當前modCount,以滿足fail-fast
            expectedModCount = modCount;
            //當前節點為null
            current = null;
        }
        //判斷是否還有next
        public final boolean hasNext() {
            //就是判斷next是否為null,預設next是head  表頭
            return next != null;
        }
        //nextNode() 就是迭代器裡的next()方法 。
        //該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙連結串列的表頭開始迴圈輸出。
        final LinkedHashMap.Entry<K,V> nextNode() {
            //記錄要返回的e。
            LinkedHashMap.Entry<K,V> e = next;
            //判斷fail-fast
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //如果要返回的節點是null,異常
            if (e == null)
                throw new NoSuchElementException();
            //更新當前節點為e
            current = e;
            //更新下一個節點是e的後置節點
            next = e.after;
            //返回e
            return e;
        }
        //刪除方法 最終還是呼叫了HashMap的removeNode方法
        public final void remove() {
            Node<K,V> p = current;
            if (p == null)
                throw new IllegalStateException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            current = null;
            K key = p.key;
            removeNode(hash(key), key, null, false, false);
            expectedModCount = modCount;
        }
    }複製程式碼

值得注意的就是:nextNode() 就是迭代器裡的next()方法 。
該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙連結串列的表頭開始迴圈輸出
而雙連結串列節點的順序在LinkedHashMap增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。

總結

LinkedHashMap相對於HashMap的原始碼比,是很簡單的。因為大樹底下好乘涼。它繼承了HashMap,僅重寫了幾個方法,以改變它迭代遍歷時的順序。這也是其與HashMap相比最大的不同。
在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。

  • accessOrder ,預設是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。為true時,可以在這基礎之上構建一個LruCache.
  • LinkedHashMap並沒有重寫任何put方法。但是其重寫了構建新節點的newNode()方法.在每次構建新節點時,將新節點連結在內部雙向連結串列的尾部
  • accessOrder=true的模式下,在afterNodeAccess()函式中,會將當前被訪問到的節點e,移動至內部的雙向連結串列的尾部。值得注意的是,afterNodeAccess()函式中,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時,如果同時查詢訪問資料,也會導致fail-fast,因為迭代的順序已經改變。
  • nextNode() 就是迭代器裡的next()方法 。
    該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙連結串列的表頭開始迴圈輸出
    而雙連結串列節點的順序在LinkedHashMap增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。
  • 它與HashMap比,還有一個小小的優化,重寫了containsValue()方法,直接遍歷內部連結串列去比對value值是否相等。

那麼,還有最後一個小問題?為什麼它不重寫containsKey()方法,也去迴圈比對內部連結串列的key是否相等呢?

相關文章