LinkedHashMap 的實(shí)現(xiàn)原理(分享)

LinkedHashMap 的實(shí)現(xiàn)原理

LinkedHashMap 概述

HashMap 是無序的,HashMap 在 put 的時(shí)候是根據(jù) key 的 hashcode 進(jìn)行 hash 然后放入對(duì)應(yīng)的地方。所以在按照一定順序 put 進(jìn) HashMap 中笛厦,然后遍歷出 HashMap 的順序跟 put 的順序不同(除非在 put 的時(shí)候 key 已經(jīng)按照 hashcode 排序號(hào)了赎瞎,這種幾率非常型慊恪)

JAVA 在 JDK1.4 以后提供了 LinkedHashMap 來幫助我們實(shí)現(xiàn)了有序的 HashMap!

LinkedHashMap 是 HashMap 的一個(gè)子類,它保留插入的順序访诱,如果需要輸出的順序和輸入時(shí)的相同,那么就選用 LinkedHashMap韩肝。

LinkedHashMap 是 Map 接口的哈希表和鏈接列表實(shí)現(xiàn)触菜,具有可預(yù)知的迭代順序。此實(shí)現(xiàn)提供所有可選的映射操作哀峻,并允許使用 null 值和 null 鍵涡相。此類不保證映射的順序,特別是它不保證該順序恒久不變剩蟀。

LinkedHashMap 實(shí)現(xiàn)與 HashMap 的不同之處在于催蝗,LinkedHashMap 維護(hù)著一個(gè)運(yùn)行于所有條目的雙重鏈接列表。此鏈接列表定義了迭代順序育特,該迭代順序可以是插入順序或者是訪問順序丙号。

注意,此實(shí)現(xiàn)不是同步的缰冤。如果多個(gè)線程同時(shí)訪問鏈接的哈希映射犬缨,而其中至少一個(gè)線程從結(jié)構(gòu)上修改了該映射,則它必須保持外部同步棉浸。

根據(jù)鏈表中元素的順序可以分為:按插入順序的鏈表怀薛,和按訪問順序(調(diào)用 get 方法)的鏈表。默認(rèn)是按插入順序排序迷郑,如果指定按訪問順序排序枝恋,那么調(diào)用get方法后,會(huì)將這次訪問的元素移至鏈表尾部嗡害,不斷訪問可以形成按訪問順序排序的鏈表焚碌。

小 Demo

我在最開始學(xué)習(xí) LinkedHashMap 的時(shí)候,看到訪問順序就漾、插入順序等等呐能,有點(diǎn)暈了,隨著后續(xù)的學(xué)習(xí)才慢慢懂得其中原理,所以我會(huì)先在進(jìn)行做幾個(gè) demo 來演示一下 LinkedHashMap 的使用摆出±驶玻看懂了其效果,然后再來研究其原理偎漫。

HashMap

看下面這個(gè)代碼:

public static void main(String[] args) {

? ? Map map = new HashMap();

? ? map.put("apple", "蘋果");

? ? map.put("watermelon", "西瓜");

? ? map.put("banana", "香蕉");

? ? map.put("peach", "桃子");

? ? Iterator iter = map.entrySet().iterator();

? ? while (iter.hasNext()) {

? ? ? ? Map.Entry entry = (Map.Entry) iter.next();

? ? ? ? System.out.println(entry.getKey() + "=" + entry.getValue());

? ? }

}

一個(gè)比較簡單的測試 HashMap 的代碼爷恳,通過控制臺(tái)的輸出,我們可以看到 HashMap 是沒有順序的象踊。

banana=香蕉

apple=蘋果

peach=桃子

watermelon=西瓜

LinkedHashMap

我們現(xiàn)在將 map 的實(shí)現(xiàn)換成 LinkedHashMap温亲,其他代碼不變:Map map = new LinkedHashMap();

看一下控制臺(tái)的輸出:

apple=蘋果

watermelon=西瓜

banana=香蕉

peach=桃子

我們可以看到,其輸出順序是完成按照插入順序的杯矩!也就是我們上面所說的保留了插入的順序栈虚。我們不是在上面還提到過其可以按照訪問順序進(jìn)行排序么?好的史隆,我們還是通過一個(gè)例子來驗(yàn)證一下:

public static void main(String[] args) {

? ? Map map = new LinkedHashMap(16,0.75f,true);

? ? map.put("apple", "蘋果");

? ? map.put("watermelon", "西瓜");

? ? map.put("banana", "香蕉");

? ? map.put("peach", "桃子");

? ? map.get("banana");

? ? map.get("apple");

? ? Iterator iter = map.entrySet().iterator();

? ? while (iter.hasNext()) {

? ? ? ? Map.Entry entry = (Map.Entry) iter.next();

? ? ? ? System.out.println(entry.getKey() + "=" + entry.getValue());

? ? }

}

代碼與之前的都差不多魂务,但我們多了兩行代碼,并且初始化 LinkedHashMap 的時(shí)候泌射,用的構(gòu)造函數(shù)也不相同粘姜,看一下控制臺(tái)的輸出結(jié)果:

watermelon=西瓜

peach=桃子

banana=香蕉

apple=蘋果

這也就是我們之前提到過的,LinkedHashMap 可以選擇按照訪問順序進(jìn)行排序熔酷。

LinkedHashMap 的實(shí)現(xiàn)

對(duì)于 LinkedHashMap 而言孤紧,它繼承與 HashMap(public class LinkedHashMap extends HashMap implements Map)、底層使用哈希表與雙向鏈表來保存所有元素拒秘。其基本操作與父類 HashMap 相似号显,它通過重寫父類相關(guān)的方法,來實(shí)現(xiàn)自己的鏈接列表特性翼抠。下面我們來分析 LinkedHashMap 的源代碼:

成員變量

LinkedHashMap 采用的 hash 算法和 HashMap 相同咙轩,但是它重新定義了數(shù)組中保存的元素 Entry,該 Entry 除了保存當(dāng)前對(duì)象的引用外阴颖,還保存了其上一個(gè)元素 before 和下一個(gè)元素 after 的引用活喊,從而在哈希表的基礎(chǔ)上又構(gòu)成了雙向鏈接列表×坷ⅲ看源代碼:

/**

* The iteration ordering method for this linked hash map: true

* for access-order, false for insertion-order.

* 如果為true钾菊,則按照訪問順序;如果為false偎肃,則按照插入順序煞烫。

*/

private final boolean accessOrder;

/**

* 雙向鏈表的表頭元素。

*/

private transient Entry header;

/**

* LinkedHashMap的Entry元素累颂。

* 繼承HashMap的Entry元素滞详,又保存了其上一個(gè)元素before和下一個(gè)元素after的引用凛俱。

*/

private static class Entry extends HashMap.Entry {

? ? Entry before, after;

? ? ……

}

LinkedHashMap 中的 Entry 集成與 HashMap 的 Entry,但是其增加了 before 和 after 的引用料饥,指的是上一個(gè)元素和下一個(gè)元素的引用蒲犬。

初始化

通過源代碼可以看出,在 LinkedHashMap 的構(gòu)造方法中岸啡,實(shí)際調(diào)用了父類 HashMap 的相關(guān)構(gòu)造方法來構(gòu)造一個(gè)底層存放的 table 數(shù)組原叮,但額外可以增加 accessOrder 這個(gè)參數(shù),如果不設(shè)置巡蘸,默認(rèn)為 false奋隶,代表按照插入順序進(jìn)行迭代;當(dāng)然可以顯式設(shè)置為 true悦荒,代表以訪問順序進(jìn)行迭代唯欣。如:

public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) {

? ? super(initialCapacity, loadFactor);

? ? this.accessOrder = accessOrder;

}

我們已經(jīng)知道 LinkedHashMap 的 Entry 元素繼承 HashMap 的 Entry,提供了雙向鏈表的功能逾冬。在上述 HashMap 的構(gòu)造器中黍聂,最后會(huì)調(diào)用 init() 方法,進(jìn)行相關(guān)的初始化身腻,這個(gè)方法在 HashMap 的實(shí)現(xiàn)中并無意義,只是提供給子類實(shí)現(xiàn)相關(guān)的初始化調(diào)用匹厘。

但在 LinkedHashMap 重寫了 init() 方法嘀趟,在調(diào)用父類的構(gòu)造方法完成構(gòu)造后,進(jìn)一步實(shí)現(xiàn)了對(duì)其元素 Entry 的初始化操作愈诚。

/**

* Called by superclass constructors and pseudoconstructors (clone,

* readObject) before any entries are inserted into the map.? Initializes

* the chain.

*/

@Override

void init() {

? header = new Entry<>(-1, null, null, null);

? header.before = header.after = header;

}

存儲(chǔ)

LinkedHashMap 并未重寫父類 HashMap 的 put 方法她按,而是重寫了父類 HashMap 的 put 方法調(diào)用的子方法void recordAccess(HashMap m) ,void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex)炕柔,提供了自己特有的雙向鏈接列表的實(shí)現(xiàn)酌泰。我們?cè)谥暗奈恼轮幸呀?jīng)講解了HashMap的put方法,我們?cè)谶@里重新貼一下 HashMap 的 put 方法的源代碼:

HashMap.put:

public V put(K key, V value) {

? ? ? ? if (key == null)

? ? ? ? ? ? return putForNullKey(value);

? ? ? ? int hash = hash(key);

? ? ? ? int i = indexFor(hash, table.length);

? ? ? ? for (Entry e = table[i]; e != null; e = e.next) {

? ? ? ? ? ? Object k;

? ? ? ? ? ? if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {

? ? ? ? ? ? ? ? V oldValue = e.value;

? ? ? ? ? ? ? ? e.value = value;

? ? ? ? ? ? ? ? e.recordAccess(this);

? ? ? ? ? ? ? ? return oldValue;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? modCount++;

? ? ? ? addEntry(hash, key, value, i);

? ? ? ? return null;

}

重寫方法:

void recordAccess(HashMap m) {

? ? LinkedHashMap lm = (LinkedHashMap)m;

? ? if (lm.accessOrder) {

? ? ? ? lm.modCount++;

? ? ? ? remove();

? ? ? ? addBefore(lm.header);

? ? ? ? }

}

void addEntry(int hash, K key, V value, int bucketIndex) {

? ? // 調(diào)用create方法匕累,將新元素以雙向鏈表的的形式加入到映射中陵刹。

? ? createEntry(hash, key, value, bucketIndex);

? ? // 刪除最近最少使用元素的策略定義

? ? Entry eldest = header.after;

? ? if (removeEldestEntry(eldest)) {

? ? ? ? removeEntryForKey(eldest.key);

? ? } else {

? ? ? ? if (size >= threshold)

? ? ? ? ? ? resize(2 * table.length);

? ? }

}

void createEntry(int hash, K key, V value, int bucketIndex) {

? ? HashMap.Entry old = table[bucketIndex];

? ? Entry e = new Entry(hash, key, value, old);

? ? table[bucketIndex] = e;

? ? // 調(diào)用元素的addBrefore方法,將元素加入到哈希欢嘿、雙向鏈接列表衰琐。?

? ? e.addBefore(header);

? ? size++;

}

private void addBefore(Entry existingEntry) {

? ? after? = existingEntry;

? ? before = existingEntry.before;

? ? before.after = this;

? ? after.before = this;

}

讀取

LinkedHashMap 重寫了父類 HashMap 的 get 方法,實(shí)際在調(diào)用父類 getEntry() 方法取得查找的元素后炼蹦,再判斷當(dāng)排序模式 accessOrder 為 true 時(shí)羡宙,記錄訪問順序,將最新訪問的元素添加到雙向鏈表的表頭掐隐,并從原來的位置刪除狗热。由于的鏈表的增加、刪除操作是常量級(jí)的,故并不會(huì)帶來性能的損失匿刮。

public V get(Object key) {

? ? // 調(diào)用父類HashMap的getEntry()方法僧凰,取得要查找的元素。

? ? Entry e = (Entry)getEntry(key);

? ? if (e == null)

? ? ? ? return null;

? ? // 記錄訪問順序僻焚。

? ? e.recordAccess(this);

? ? return e.value;

}

void recordAccess(HashMap m) {

? ? LinkedHashMap lm = (LinkedHashMap)m;

? ? // 如果定義了LinkedHashMap的迭代順序?yàn)樵L問順序允悦,

? ? // 則刪除以前位置上的元素,并將最新訪問的元素添加到鏈表表頭虑啤。?

? ? if (lm.accessOrder) {

? ? ? ? lm.modCount++;

? ? ? ? remove();

? ? ? ? addBefore(lm.header);

? ? }

}

/**

* Removes this entry from the linked list.

*/

private void remove() {

? ? before.after = after;

? ? after.before = before;

}

/**clear鏈表隙弛,設(shè)置header為初始狀態(tài)*/

public void clear() {

super.clear();

header.before = header.after = header;

}

排序模式

LinkedHashMap 定義了排序模式 accessOrder,該屬性為 boolean 型變量狞山,對(duì)于訪問順序全闷,為 true;對(duì)于插入順序萍启,則為 false总珠。一般情況下,不必指定排序模式勘纯,其迭代順序即為默認(rèn)為插入順序局服。

這些構(gòu)造方法都會(huì)默認(rèn)指定排序模式為插入順序。如果你想構(gòu)造一個(gè) LinkedHashMap驳遵,并打算按從近期訪問最少到近期訪問最多的順序(即訪問順序)來保存元素淫奔,那么請(qǐng)使用下面的構(gòu)造方法構(gòu)造 LinkedHashMap:public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

該哈希映射的迭代順序就是最后訪問其條目的順序,這種映射很適合構(gòu)建 LRU 緩存堤结。LinkedHashMap 提供了 removeEldestEntry(Map.Entry eldest) 方法唆迁。該方法可以提供在每次添加新條目時(shí)移除最舊條目的實(shí)現(xiàn)程序,默認(rèn)返回 false竞穷,這樣唐责,此映射的行為將類似于正常映射,即永遠(yuǎn)不能移除最舊的元素瘾带。

我們會(huì)在后面的文章中詳細(xì)介紹關(guān)于如何用 LinkedHashMap 構(gòu)建 LRU 緩存鼠哥。

總結(jié)

其實(shí) LinkedHashMap 幾乎和 HashMap 一樣:從技術(shù)上來說,不同的是它定義了一個(gè) Entry header月弛,這個(gè) header 不是放在 Table 里肴盏,它是額外獨(dú)立出來的。LinkedHashMap 通過繼承 hashMap 中的 Entry,并添加兩個(gè)屬性 Entry before,after,和 header 結(jié)合起來組成一個(gè)雙向鏈表帽衙,來實(shí)現(xiàn)按插入順序或訪問順序排序菜皂。

在寫關(guān)于 LinkedHashMap 的過程中,記起來之前面試的過程中遇到的一個(gè)問題厉萝,也是問我 Map 的哪種實(shí)現(xiàn)可以做到按照插入順序進(jìn)行迭代恍飘?當(dāng)時(shí)腦子是突然短路的榨崩,但現(xiàn)在想想,也只能怪自己對(duì)這個(gè)知識(shí)點(diǎn)還是掌握的不夠扎實(shí)章母,所以又從頭認(rèn)真的把代碼看了一遍母蛛。

不過,我的建議是乳怎,大家首先首先需要記住的是:LinkedHashMap 能夠做到按照插入順序或者訪問順序進(jìn)行迭代彩郊,這樣在我們以后的開發(fā)中遇到相似的問題,才能想到用 LinkedHashMap 來解決蚪缀,否則就算對(duì)其內(nèi)部結(jié)構(gòu)非常了解秫逝,不去使用也是沒有什么用的。

我們學(xué)習(xí)的目的是為了更好的應(yīng)用询枚。

出處:http://wiki.jikexueyuan.com/project/java-collection/linkedhashmap.html

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末违帆,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子金蜀,更是在濱河造成了極大的恐慌刷后,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,843評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渊抄,死亡現(xiàn)場離奇詭異尝胆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)护桦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,538評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門班巩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嘶炭,你說我怎么就攤上這事⊙疯耄” “怎么了眨猎?”我有些...
    開封第一講書人閱讀 163,187評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長强经。 經(jīng)常有香客問我睡陪,道長,這世上最難降的妖魔是什么匿情? 我笑而不...
    開封第一講書人閱讀 58,264評(píng)論 1 292
  • 正文 為了忘掉前任兰迫,我火速辦了婚禮,結(jié)果婚禮上炬称,老公的妹妹穿的比我還像新娘汁果。我一直安慰自己,他們只是感情好玲躯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,289評(píng)論 6 390
  • 文/花漫 我一把揭開白布据德。 她就那樣靜靜地躺著鳄乏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棘利。 梳的紋絲不亂的頭發(fā)上橱野,一...
    開封第一講書人閱讀 51,231評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音善玫,去河邊找鬼水援。 笑死,一個(gè)胖子當(dāng)著我的面吹牛茅郎,可吹牛的內(nèi)容都是我干的蜗元。 我是一名探鬼主播,決...
    沈念sama閱讀 40,116評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼只洒,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼许帐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起毕谴,我...
    開封第一講書人閱讀 38,945評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤成畦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后涝开,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體循帐,經(jīng)...
    沈念sama閱讀 45,367評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,581評(píng)論 2 333
  • 正文 我和宋清朗相戀三年舀武,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了拄养。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,754評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡银舱,死狀恐怖瘪匿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情寻馏,我是刑警寧澤棋弥,帶...
    沈念sama閱讀 35,458評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站诚欠,受9級(jí)特大地震影響顽染,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜轰绵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,068評(píng)論 3 327
  • 文/蒙蒙 一粉寞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧左腔,春花似錦唧垦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,692評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽野芒。三九已至,卻和暖如春双炕,著一層夾襖步出監(jiān)牢的瞬間狞悲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,842評(píng)論 1 269
  • 我被黑心中介騙來泰國打工妇斤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留摇锋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,797評(píng)論 2 369
  • 正文 我出身青樓站超,卻偏偏與公主長得像荸恕,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子死相,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,654評(píng)論 2 354

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