09-LinkedHashMap 核心源碼分析(集合)

注:源碼系列文章主要是對(duì)某付費(fèi)專欄的總結(jié)記錄瑞佩。如有侵權(quán),請(qǐng)聯(lián)系刪除垦缅。

1 LinkedHashMap 整體架構(gòu)

HashMap 是無序的自赔,TreeMap 可以按照 key 進(jìn)行排序,那有木有 Map 是可以維護(hù)插入的順序的呢背犯?接下來我們看看 LinkedHashMap坏瘩。

LinkedHashMap 本身是繼承 HashMap 的,所以它擁有 HashMap 的所有特性漠魏,在此基礎(chǔ)上倔矾,還提供了兩大特性:

  • 按照插入順序進(jìn)行訪問;
  • 實(shí)現(xiàn)了訪問最小最先刪除功能,其目的是把很久都沒有訪問的 key 自動(dòng)刪除哪自。

1.1 按照插入順序訪問

1.1.1 LinkedHashMap 鏈表結(jié)構(gòu)

// 鏈表頭
transient LinkedHashMap.Entry<K,V> head;

// 鏈表尾
transient LinkedHashMap.Entry<K,V> tail;

// 繼承 Node丰包,為數(shù)組的每個(gè)元素增加了 before 和 after 屬性
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);
    }
}

// 控制兩種訪問模式的字段,默認(rèn) false
// true: 按照訪問順序提陶,會(huì)把經(jīng)常訪問的 key 放到隊(duì)尾
// false: 按照插入順序提供訪問
final boolean accessOrder;

從新增的屬性可以看到,LinkedHashMap 的數(shù)據(jù)結(jié)構(gòu)很像是把 LinkedList 每個(gè)元素?fù)Q成了 HashMap 的 Node匹层,像是兩者的結(jié)合體隙笆,也正是因?yàn)樵黾恿诉@些結(jié)構(gòu),從而能把 Map 的元素都串聯(lián)起來升筏,形成一個(gè)鏈表撑柔,而鏈表就可以保證順序了,就可以維護(hù)元素插入進(jìn)來的順序您访。

1.1.2 按照順序新增

LinkedHashMap 初始化時(shí)铅忿,默認(rèn)的 accessOrder 是 false,就是會(huì)按照插入順序提供訪問灵汪,插入方法使用的是父類 HashMap 的 put 方法檀训,不過覆寫了 put 方法執(zhí)行中調(diào)用的 newNode/newTreeNode 和 afterNodeAccess 方法。

newNode/newTreeNode 方法享言,控制新增節(jié)點(diǎn)追加到鏈表的尾部峻凫,這樣每次新節(jié)點(diǎn)都追加到尾部,即可保證插入順序了览露,我們以 newNode 源碼為例:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // 初始化一個(gè)新增的節(jié)點(diǎn)
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 追加到鏈表的尾部
    linkNodeLast(p);
    return p;
}

// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 緩存舊的尾節(jié)點(diǎn)
    LinkedHashMap.Entry<K,V> last = tail;
    // 賦值尾節(jié)點(diǎn)為新增節(jié)點(diǎn)
    tail = p;
    // 如果舊的尾節(jié)點(diǎn)為null則表示當(dāng)前鏈表為空荧琼,直接將新增節(jié)點(diǎn)賦值于頭節(jié)點(diǎn)
    if (last == null)
        head = p;
    // 鏈表有數(shù)據(jù),將舊的尾節(jié)點(diǎn)和新的尾節(jié)點(diǎn)互聯(lián)
    else {
        p.before = last;
        last.after = p;
    }
}

LinkedHashMap 通過新增頭節(jié)點(diǎn)差牛、尾節(jié)點(diǎn)命锄,給每個(gè)節(jié)點(diǎn)增加 before、after 屬性偏化,每次新增時(shí)脐恩,都把節(jié)點(diǎn)追加到尾節(jié)點(diǎn)等手段,在新增的時(shí)候侦讨,就已經(jīng)維護(hù)了按照插入順序的鏈表結(jié)構(gòu)了被盈。

1.1.3 按照順序訪問

LinkedHashMap 只提供了單向訪問,即按照插入的順序從頭到尾進(jìn)行訪問搭伤,不能像 LinkedList 那樣可以雙向訪問只怎。

我們主要通過迭代器進(jìn)行訪問,迭代器初始化的時(shí)候怜俐,默認(rèn)從頭節(jié)點(diǎn)開始訪問身堡,在迭代的過程中,不斷訪問當(dāng)前節(jié)點(diǎn)的 after 節(jié)點(diǎn)即可拍鲤。

Map 對(duì) key贴谎、value 和 entity(節(jié)點(diǎn))都提供了迭代的方法汞扎,假設(shè)women攜帶 entity,就可以使用 LinkedHashMap.entrySet().iterator() 這種寫法直接返回 LinkedHashIterator擅这,LinkedHashIterator 是迭代器澈魄,我們通過調(diào)用迭代器的 nextNode() 方法就可以得到下一個(gè)節(jié)點(diǎn),迭代器的源碼如下:

abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    int expectedModCount;

    // 初始化時(shí)仲翎,默認(rèn)從頭節(jié)點(diǎn)開始訪問
    LinkedHashIterator() {
        next = head;
        expectedModCount = modCount;
        current = null;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final LinkedHashMap.Entry<K,V> nextNode() {
        // 當(dāng)前遍歷的節(jié)點(diǎn)
        LinkedHashMap.Entry<K,V> e = next;
        // 判斷 modCount
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        // 通過鏈表的 after 結(jié)構(gòu)痹扇,找到下一個(gè)迭代的節(jié)點(diǎn)
        next = e.after;
        return e;
    }
}

在新增節(jié)點(diǎn)時(shí),我們就已經(jīng)維護(hù)了元素之間的插入順序了溯香,所以迭代訪問是非常簡(jiǎn)答的鲫构,只需要不斷的訪問當(dāng)前節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)即可。

1.2 訪問最少刪除策略

1.2.1 演示

這種策略也叫做 <a >LRU</a>(least recently used玫坛,最近最少使用)结笨,大概的意思就是經(jīng)常訪問的元素會(huì)被追加到隊(duì)尾,這樣不經(jīng)常訪問的數(shù)據(jù)自然就靠近隊(duì)頭湿镀,然后我們可以設(shè)置刪除策略炕吸,比如當(dāng) Map 元素個(gè)數(shù)大于多少時(shí),把頭節(jié)點(diǎn)刪除勉痴,如下:

@Test
public void accessOrderTest() {
    // 使用帶參構(gòu)造函數(shù)算途,分別指定初始化大小,加載因子蚀腿,訪問模式(accessOrder)
    LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(5, 0.75f, true) {
        {
            put(1, 1);
            put(2, 2);
            put(3, 3);
            put(4, 4);
            put(5, 5);
        }

        // 覆寫了刪除策略方法嘴瓤,我們?cè)O(shè)定當(dāng)節(jié)點(diǎn)個(gè)數(shù)大于 4 時(shí),就開始刪除頭節(jié)點(diǎn)
        @Override
        protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
            return size() > 4;
        }
    };

    System.out.println("初始化: " + JSON.toJSONString(map));
    map.get(3);
    System.out.println("map.get(3): " + JSON.toJSONString(map));
    map.get(2);
    System.out.println("map.get(2): " + JSON.toJSONString(map));
}

執(zhí)行結(jié)果:

初始化: {2:2,3:3,4:4,5:5}
map.get(3): {2:2,4:4,5:5,3:3}
map.get(2): {4:4,5:5,3:3,2:2}

可以看到莉钙,map 初始化的時(shí)候廓脆,我們放進(jìn)去五個(gè)元素,但結(jié)果只有四個(gè)元素磁玉, 1 不見了停忿,這個(gè)主要是因?yàn)槲覀兏矊懥?removeEldestEntry 方法,我們實(shí)現(xiàn)了如果 map 中元素個(gè)數(shù)大于 4 時(shí)蚊伞,則就把隊(duì)頭的元素刪除席赂,當(dāng) put(5,5) 執(zhí)行的時(shí)候,正好把隊(duì)頭的 1 刪除时迫,這個(gè)體現(xiàn)了當(dāng)達(dá)到我們?cè)O(shè)定的刪除策略時(shí)颅停,會(huì)自動(dòng)刪除頭節(jié)點(diǎn)。

當(dāng)我們調(diào)用 map.get(3) 方法時(shí)掠拳,元素 3 移動(dòng)到了隊(duì)尾癞揉,調(diào)用 map.get(2) 方法時(shí),元素 2 被移動(dòng)到了隊(duì)尾,這個(gè)體現(xiàn)了經(jīng)常被訪問的節(jié)點(diǎn)會(huì)被移動(dòng)到隊(duì)尾喊熟。

這個(gè)例子就很好的說明了訪問最少刪除策略柏肪,接下來看看原理。

1.2.2 元素被轉(zhuǎn)移到隊(duì)尾

為什么 get 時(shí)芥牌,元素會(huì)被移動(dòng)到隊(duì)尾:

public V get(Object key) {
    Node<K,V> e;
    // 調(diào)用 HashMap get 方法
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果設(shè)置了 LRU 策略
    if (accessOrder)
        // 這個(gè)方法把當(dāng)前 key 移動(dòng)到隊(duì)尾
        afterNodeAccess(e);
    return e.value;
}

從上述源碼中烦味,可以看到,通過 afterNodeAccess 方法把當(dāng)前訪問節(jié)點(diǎn)移動(dòng)到了隊(duì)尾壁拉,其實(shí)不僅僅是 get 方法谬俄,執(zhí)行 getOrDefaultcompute扇商、computeIfAbsent凤瘦、computeIfPresent宿礁、merge 方法時(shí)案铺,也會(huì)這么做,通過不斷的把經(jīng)常訪問的節(jié)點(diǎn)移動(dòng)到隊(duì)尾梆靖,那么靠近隊(duì)頭的節(jié)點(diǎn)控汉,自然就是很少被訪問的元素了。

1.2.3 刪除策略

上述例子中我們?cè)趫?zhí)行 put 方法時(shí)返吻,發(fā)現(xiàn)隊(duì)頭元素被刪除了姑子,LinkedHashMap 本身是沒有 put 方法實(shí)現(xiàn)的,調(diào)用的是 HashMap 的 put 方法测僵,但 LinkedHashMap 實(shí)現(xiàn)了 put 方法中的調(diào)用 afterNodeInsertion 方法街佑,這個(gè)方法實(shí)現(xiàn)了刪除,源碼如下:

// 刪除很少被訪問的元素捍靠,被 HashMap 的 put 方法所調(diào)用
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    // 得到鏈表的頭節(jié)點(diǎn)
    LinkedHashMap.Entry<K,V> first;
    // 如果隊(duì)列不為空沐旨,并且刪除策略允許的情況下. removeEldestEntry 為我們覆寫的刪除策略
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        // 得到頭節(jié)點(diǎn)的 key
        K key = first.key;
        // 刪除頭節(jié)點(diǎn)
        removeNode(hash(key), key, null, false, true);
    }
}

1.3 小結(jié)

LinkedHashMap 提供了兩個(gè)很有意思的功能:按照插入順序訪問和刪除最少訪問元素策略,簡(jiǎn)單的通過鏈表的結(jié)構(gòu)就實(shí)現(xiàn)了榨婆,設(shè)計(jì)的非常巧妙磁携。

LinkedHashMap 在 HashMap 的基礎(chǔ)上簡(jiǎn)單的增加了鏈表結(jié)構(gòu),就形成了節(jié)點(diǎn)的順序良风,非常巧妙谊迄。

------------------------------------- END -------------------------------------

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市烟央,隨后出現(xiàn)的幾起案子统诺,更是在濱河造成了極大的恐慌,老刑警劉巖疑俭,帶你破解...
    沈念sama閱讀 222,946評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件篙议,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鬼贱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,336評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門移怯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人这难,你說我怎么就攤上這事舟误。” “怎么了姻乓?”我有些...
    開封第一講書人閱讀 169,716評(píng)論 0 364
  • 文/不壞的土叔 我叫張陵嵌溢,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我蹋岩,道長(zhǎng)赖草,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,222評(píng)論 1 300
  • 正文 為了忘掉前任剪个,我火速辦了婚禮秧骑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扣囊。我一直安慰自己乎折,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,223評(píng)論 6 398
  • 文/花漫 我一把揭開白布侵歇。 她就那樣靜靜地躺著骂澄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惕虑。 梳的紋絲不亂的頭發(fā)上坟冲,一...
    開封第一講書人閱讀 52,807評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音溃蔫,去河邊找鬼健提。 笑死,一個(gè)胖子當(dāng)著我的面吹牛酒唉,可吹牛的內(nèi)容都是我干的矩桂。 我是一名探鬼主播,決...
    沈念sama閱讀 41,235評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼痪伦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼侄榴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起网沾,我...
    開封第一講書人閱讀 40,189評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤癞蚕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后辉哥,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桦山,經(jīng)...
    沈念sama閱讀 46,712評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攒射,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,775評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了恒水。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片会放。...
    茶點(diǎn)故事閱讀 40,926評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖钉凌,靈堂內(nèi)的尸體忽然破棺而出咧最,到底是詐尸還是另有隱情,我是刑警寧澤御雕,帶...
    沈念sama閱讀 36,580評(píng)論 5 351
  • 正文 年R本政府宣布矢沿,位于F島的核電站,受9級(jí)特大地震影響酸纲,放射性物質(zhì)發(fā)生泄漏捣鲸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,259評(píng)論 3 336
  • 文/蒙蒙 一闽坡、第九天 我趴在偏房一處隱蔽的房頂上張望栽惶。 院中可真熱鬧,春花似錦无午、人聲如沸媒役。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,750評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至交惯,卻和暖如春次泽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背席爽。 一陣腳步聲響...
    開封第一講書人閱讀 33,867評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工意荤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人只锻。 一個(gè)月前我還...
    沈念sama閱讀 49,368評(píng)論 3 379
  • 正文 我出身青樓玖像,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親齐饮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子捐寥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,930評(píng)論 2 361

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

  • 移步數(shù)據(jù)結(jié)構(gòu)--容器匯總(java & Android)分析 LinkedHashMap的源碼: LinkedHa...
    凱玲之戀閱讀 561評(píng)論 0 4
  • Java集合類可用于存儲(chǔ)數(shù)量不等的對(duì)象,并可以實(shí)現(xiàn)常用的數(shù)據(jù)結(jié)構(gòu)如棧,隊(duì)列等,Java集合還可以用于保存具有映射關(guān)...
    小徐andorid閱讀 1,946評(píng)論 0 13
  • LinkedHashMap 源碼分析 上周學(xué)習(xí)了 HashMap 的源碼感覺收獲頗多,雖然紅黑樹這個(gè)坑自己還沒有填...
    醒著的碼者閱讀 565評(píng)論 0 5
  • 最近,用”直下承當(dāng)捺僻,就事論事”八個(gè)字處理一切事乡洼,覺得格局凈爽許多崇裁。 一些細(xì)微之處的改變就是: 表達(dá)情感的時(shí)候:更主...
    祝你玉樹臨風(fēng)閱讀 1,949評(píng)論 0 1
  • 描寫一個(gè)工匠走出太行砥礪前行持續(xù)奮斗的感人故事。 問君食可足束昵,謂君衣可暖拔稳,心念不敢對(duì)君語(yǔ),恐君有所牽锹雏,卿今隨軍往壳炎,...
    銀河謎米閱讀 130評(píng)論 0 2