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

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

1 概述

上文中仍秤,我們已經(jīng)聊過了HashMap,本篇是基于上文的基礎之上无牵。所以如果沒看過上文关划,請先閱讀面試必備:HashMap源碼解析(JDK8)
本文將從幾個常用方法下手煤蹭,來閱讀LinkedHashMap的源碼宰缤。
按照從構(gòu)造方法->常用API(增、刪衍腥、改绪穆、查)的順序來閱讀源碼,并會講解閱讀方法中涉及的一些變量的意義蓖扑。了解LinkedHashMap的特點唉铜、適用場景。

如果本文中有不正確的結(jié)論律杠、說法打毛,請大家提出和我討論,共同進步俩功,謝謝幻枉。

2 概要

概括的說,LinkedHashMap 是一個關聯(lián)數(shù)組诡蜓、哈希表熬甫,它是線程不安全的,允許key為null,value為null蔓罚。
它繼承自HashMap椿肩,實現(xiàn)了Map<K,V>接口瞻颂。其內(nèi)部還維護了一個雙向鏈表,在每次插入數(shù)據(jù)郑象,或者訪問贡这、修改數(shù)據(jù)時,會增加節(jié)點厂榛、或調(diào)整鏈表的節(jié)點順序盖矫。以決定迭代時輸出的順序。

默認情況击奶,遍歷時的順序是按照插入節(jié)點的順序辈双。這也是其與HashMap最大的區(qū)別。
也可以在構(gòu)造時傳入accessOrder參數(shù)柜砾,使得其遍歷順序按照訪問的順序輸出湃望。

因繼承自HashMap,所以HashMap上文分析的特點,除了輸出無序痰驱,其他LinkedHashMap都有证芭,比如擴容的策略,哈希桶長度一定是2的N次方等等担映。
LinkedHashMap在實現(xiàn)時檩帐,就是重寫override了幾個方法。以滿足其輸出序列有序的需求另萤。

示例代碼:

根據(jù)這段實例代碼湃密,先從現(xiàn)象看一下LinkedHashMap的特征:
在每次插入數(shù)據(jù),或者訪問四敞、修改數(shù)據(jù)時泛源,會增加節(jié)點、或調(diào)整鏈表的節(jié)點順序忿危。以決定迭代時輸出的順序达箍。

        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移動到了內(nèi)部的鏈表末尾
        map.get("4");//4調(diào)整至末尾
        map.put("3", "e");//3調(diào)整至末尾
        map.put(null, null);//插入兩個新的節(jié)點 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 節(jié)點

LinkedHashMap的節(jié)點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,分別指向內(nèi)部雙向鏈表的表頭、表尾解滓。

    //雙向鏈表的頭結(jié)點
    transient LinkedHashMap.Entry<K,V> head;

    //雙向鏈表的尾節(jié)點
    transient LinkedHashMap.Entry<K,V> tail;

4 構(gòu)造函數(shù)

    //默認是false赃磨,則迭代時輸出的順序是插入節(jié)點的順序。若為true洼裤,則輸出的順序是按照訪問節(jié)點的順序邻辉。
    //為true時,可以在這基礎之上構(gòu)建一個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;
    }
    //指定初始化時的容量莹菱,和擴容的加載因子,以及迭代輸出節(jié)點的順序
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    //利用另一個Map 來構(gòu)建吱瘩,
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        //該方法上文分析過道伟,批量插入一個map中的所有數(shù)據(jù)到 本集合中。
        putMapEntries(m, false);
    }
    

小結(jié):
構(gòu)造函數(shù)和HashMap相比使碾,就是增加了一個accessOrder參數(shù)蜜徽。用于控制迭代時的節(jié)點順序。

5 增

LinkedHashMap并沒有重寫任何put方法部逮。但是其重寫了構(gòu)建新節(jié)點的newNode()方法.
newNode()會在HashMapputVal()方法里被調(diào)用娜汁,putVal()方法會在批量插入數(shù)據(jù)putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入單個數(shù)據(jù)public V put(K key, V value)時被調(diào)用嫂易。

LinkedHashMap重寫了newNode(),在每次構(gòu)建新節(jié)點時兄朋,通過linkNodeLast(p);新節(jié)點鏈接在內(nèi)部雙向鏈表的尾部

    //在構(gòu)建新節(jié)點時怜械,構(gòu)建的是`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;
    }
    //將新增的節(jié)點颅和,連接在鏈表的尾部
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        //集合之前是空的
        if (last == null)
            head = p;
        else {//將新節(jié)點連接在鏈表的尾部
            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) { }
    //回調(diào)函數(shù)缕允,新節(jié)點插入之后回調(diào) 峡扩, 根據(jù)evict 和   判斷是否需要刪除最老插入的節(jié)點。如果實現(xiàn)LruCache會用到這個方法障本。
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //LinkedHashMap 默認返回false 則不刪除節(jié)點
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    //LinkedHashMap 默認返回false 則不刪除節(jié)點教届。 返回true 代表要刪除最早的節(jié)點。通常構(gòu)建一個LruCache會在達到Cache的上限是返回true
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是構(gòu)建LruCache需要的回調(diào)驾霜,在LinkedHashMap里可以忽略它們案训。

6 刪

LinkedHashMap也沒有重寫remove()方法,因為它的刪除邏輯和HashMap并無區(qū)別粪糙。
但它重寫了afterNodeRemoval()這個回調(diào)方法强霎。該方法會在Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法中回調(diào),removeNode()會在所有涉及到刪除節(jié)點的方法中被調(diào)用蓉冈,上文分析過城舞,是刪除節(jié)點操作的真正執(zhí)行者。

    //在刪除節(jié)點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;
        //待刪除節(jié)點 p 的前置后置節(jié)點都置空
        p.before = p.after = null;
        //如果前置節(jié)點是null家夺,則現(xiàn)在的頭結(jié)點應該是后置節(jié)點a
        if (b == null)
            head = a;
        else//否則將前置節(jié)點b的后置節(jié)點指向a
            b.after = a;
        //同理如果后置節(jié)點時null ,則尾節(jié)點應是b
        if (a == null)
            tail = b;
        else//否則更新后置節(jié)點a的前置節(jié)點為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中的實現(xiàn),LinkedHashMap只是增加了在成員變量(構(gòu)造函數(shù)時賦值)accessOrder為true的情況下伐弹,要去回調(diào)void afterNodeAccess(Node<K,V> e)函數(shù)秦踪。

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

afterNodeAccess()函數(shù)中,會將當前被訪問到的節(jié)點e,移動至內(nèi)部的雙向鏈表的尾部椅邓。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;//原尾節(jié)點
        //如果accessOrder 是true 柠逞,且原尾節(jié)點不等于e
        if (accessOrder && (last = tail) != e) {
            //節(jié)點e強轉(zhuǎn)成雙向鏈表節(jié)點p
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //p現(xiàn)在是尾節(jié)點, 后置節(jié)點一定是null
            p.after = null;
            //如果p的前置節(jié)點是null景馁,則p以前是頭結(jié)點板壮,所以更新現(xiàn)在的頭結(jié)點是p的后置節(jié)點a
            if (b == null)
                head = a;
            else//否則更新p的前直接點b的后置節(jié)點為 a
                b.after = a;
            //如果p的后置節(jié)點不是null,則更新后置節(jié)點a的前置節(jié)點為b
            if (a != null)
                a.before = b;
            else//如果原本p的后置節(jié)點是null合住,則p就是尾節(jié)點绰精。 此時 更新last的引用為 p的前置節(jié)點b
                last = b;
            if (last == null) //原本尾節(jié)點是null  則,鏈表中就一個節(jié)點
                head = p;
            else {//否則 更新 當前節(jié)點p的前置節(jié)點為 原尾節(jié)點last透葛, last的后置節(jié)點是p
                p.before = last;
                last.after = p;
            }
            //尾節(jié)點的引用賦值成p
            tail = p;
            //修改modCount笨使。
            ++modCount;
        }
    }

值得注意的是,afterNodeAccess()函數(shù)中僚害,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時硫椰,如果同時查詢訪問數(shù)據(jù),也會導致fail-fast萨蚕,因為迭代的順序已經(jīng)改變靶草。

7.2 containsValue

它重寫了該方法,相比HashMap的實現(xiàn)岳遥,更為高效奕翔。

    public boolean containsValue(Object value) {
        //遍歷一遍鏈表,去比較有沒有value相等的節(jié)點浩蓉,并返回
        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循環(huán)遍歷,相對低效捻艳。

    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 {
        //下一個節(jié)點
        LinkedHashMap.Entry<K,V> next;
        //當前節(jié)點
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            //初始化時驾窟,next 為 LinkedHashMap內(nèi)部維護的雙向鏈表的扁頭
            next = head;
            //記錄當前modCount,以滿足fail-fast
            expectedModCount = modCount;
            //當前節(jié)點為null
            current = null;
        }
        //判斷是否還有next
        public final boolean hasNext() {
            //就是判斷next是否為null讯泣,默認next是head  表頭
            return next != null;
        }
        //nextNode() 就是迭代器里的next()方法 纫普。
        //該方法的實現(xiàn)可以看出,迭代LinkedHashMap好渠,就是從內(nèi)部維護的雙鏈表的表頭開始循環(huán)輸出昨稼。
        final LinkedHashMap.Entry<K,V> nextNode() {
            //記錄要返回的e。
            LinkedHashMap.Entry<K,V> e = next;
            //判斷fail-fast
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            //如果要返回的節(jié)點是null拳锚,異常
            if (e == null)
                throw new NoSuchElementException();
            //更新當前節(jié)點為e
            current = e;
            //更新下一個節(jié)點是e的后置節(jié)點
            next = e.after;
            //返回e
            return e;
        }
        //刪除方法 最終還是調(diào)用了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()方法 假栓。
該方法的實現(xiàn)可以看出,迭代LinkedHashMap霍掺,就是從內(nèi)部維護的雙鏈表的表頭開始循環(huán)輸出匾荆。
而雙鏈表節(jié)點的順序在LinkedHashMap增拌蜘、刪、改牙丽、查時都會更新简卧。以滿足按照插入順序輸出,還是訪問順序輸出烤芦。

總結(jié)

LinkedHashMap相對于HashMap的源碼比举娩,是很簡單的。因為大樹底下好乘涼构罗。它繼承了HashMap铜涉,僅重寫了幾個方法,以改變它迭代遍歷時的順序遂唧。這也是其與HashMap相比最大的不同芙代。
在每次插入數(shù)據(jù),或者訪問盖彭、修改數(shù)據(jù)時纹烹,會增加節(jié)點、或調(diào)整鏈表的節(jié)點順序谬泌。以決定迭代時輸出的順序滔韵。

  • accessOrder ,默認是false逻谦,則迭代時輸出的順序是插入節(jié)點的順序掌实。若為true,則輸出的順序是按照訪問節(jié)點的順序邦马。為true時贱鼻,可以在這基礎之上構(gòu)建一個LruCache.
  • LinkedHashMap并沒有重寫任何put方法。但是其重寫了構(gòu)建新節(jié)點的newNode()方法.在每次構(gòu)建新節(jié)點時滋将,將新節(jié)點鏈接在內(nèi)部雙向鏈表的尾部
  • accessOrder=true的模式下,在afterNodeAccess()函數(shù)中邻悬,會將當前被訪問到的節(jié)點e,移動至內(nèi)部的雙向鏈表的尾部随闽。值得注意的是父丰,afterNodeAccess()函數(shù)中,會修改modCount,因此當你正在accessOrder=true的模式下,迭代LinkedHashMap時掘宪,如果同時查詢訪問數(shù)據(jù)蛾扇,也會導致fail-fast,因為迭代的順序已經(jīng)改變魏滚。
  • nextNode() 就是迭代器里的next()方法 镀首。
    該方法的實現(xiàn)可以看出,迭代LinkedHashMap鼠次,就是從內(nèi)部維護的雙鏈表的表頭開始循環(huán)輸出更哄。
    而雙鏈表節(jié)點的順序在LinkedHashMap增芋齿、刪、改成翩、查時都會更新觅捆。以滿足按照插入順序輸出,還是訪問順序輸出麻敌。
  • 它與HashMap比惠拭,還有一個小小的優(yōu)化,重寫了containsValue()方法庸论,直接遍歷內(nèi)部鏈表去比對value值是否相等职辅。

那么,還有最后一個小問題聂示?為什么它不重寫containsKey()方法域携,也去循環(huán)比對內(nèi)部鏈表的key是否相等呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鱼喉,一起剝皮案震驚了整個濱河市秀鞭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扛禽,老刑警劉巖锋边,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異编曼,居然都是意外死亡豆巨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門掐场,熙熙樓的掌柜王于貴愁眉苦臉地迎上來往扔,“玉大人,你說我怎么就攤上這事熊户∑继牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵嚷堡,是天一觀的道長蝗罗。 經(jīng)常有香客問我,道長蝌戒,這世上最難降的妖魔是什么串塑? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮瓶颠,結(jié)果婚禮上拟赊,老公的妹妹穿的比我還像新娘。我一直安慰自己粹淋,他們只是感情好吸祟,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布瑟慈。 她就那樣靜靜地躺著,像睡著了一般屋匕。 火紅的嫁衣襯著肌膚如雪葛碧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天过吻,我揣著相機與錄音进泼,去河邊找鬼。 笑死纤虽,一個胖子當著我的面吹牛乳绕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播逼纸,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼洋措,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了杰刽?” 一聲冷哼從身側(cè)響起菠发,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贺嫂,沒想到半個月后滓鸠,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡第喳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年糜俗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墩弯。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡吩跋,死狀恐怖寞射,靈堂內(nèi)的尸體忽然破棺而出渔工,到底是詐尸還是另有隱情虏两,我是刑警寧澤坛缕,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布欣尼,位于F島的核電站烦粒,受9級特大地震影響绰疤,放射性物質(zhì)發(fā)生泄漏戴卜。R本人自食惡果不足惜胶坠,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一指么、第九天 我趴在偏房一處隱蔽的房頂上張望掏觉。 院中可真熱鬧区端,春花似錦、人聲如沸澳腹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至沥邻,卻和暖如春危虱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唐全。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工埃跷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人邮利。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓弥雹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親延届。 傳聞我的和親對象是個殘疾皇子缅糟,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容