LinkedHashMap詳細(xì)解析
概論
- LinkedHashMap 通過特有底層雙向鏈表的支持,使得LinkedHashMap可以保存元素之間的順序,例如插入順序或者訪問順序,而HashMap因?yàn)闆]有雙向鏈表的支持,所以就不能保持這種順序,所以它的訪問就是隨機(jī)的了
- 和HashMap一樣,還是通過數(shù)組存儲元素的
- 這里的順序指的是遍歷的順序,定義了頭結(jié)點(diǎn)head,當(dāng)我們調(diào)用迭代器進(jìn)行遍歷時,通過head開始遍歷,通過after屬性可以不斷找到下一個,直到tail尾結(jié)點(diǎn),從而實(shí)現(xiàn)順序性。在同一個hash(其實(shí)更準(zhǔn)確的說是同一個下標(biāo),數(shù)組index ,在上圖中表現(xiàn)了同一列)鏈表內(nèi)部next和HashMap.Node.next 的效果是一樣的。不同點(diǎn)在于before和after可以連接不同hash之間的鏈表,也就是說雙向鏈表是可以跨任何index 連接的,也就是說將LinkedHashMap里面的所有元素按照特定的順序連接起來的
LinkedHashMap 的最終形態(tài)
- 圖中的黑色箭頭表示next指針,這跟HashMap是一樣的;
- 圖中的紅色箭頭表示before和after指針,分別指向這個節(jié)點(diǎn)的前面和后面一個節(jié)點(diǎn),這就可以實(shí)現(xiàn)有序訪問;
初識LinkedHashMap
我們想在頁面展示一周內(nèi)的消費(fèi)變化情況,用echarts面積圖進(jìn)行展示。如下:
我們在后臺將數(shù)據(jù)構(gòu)造完成
然而頁面上一展示,發(fā)現(xiàn)并非如此,我們打印出來看,發(fā)現(xiàn)順序并非我們所想,先put進(jìn)去的先get出來
那么如何保證預(yù)期展示結(jié)果如我們所想呢,這個時候就需要用到LinkedHashMap實(shí)體,首先我們把上述代碼用LinkedHashMap進(jìn)行重構(gòu)
這個時候,結(jié)果正如我們所預(yù)期
key: 星期一, value: 40
key: 星期二, value: 43
key: 星期三, value: 35
key: 星期四, value: 55
key: 星期五, value: 45
key: 星期六, value: 35
key: 星期日, value: 30
LinkedHashMap 的繼承關(guān)系
繼承關(guān)系圖
LinkedHashMap繼承了HashMap類,是HashMap的子類,LinkedHashMap的大多數(shù)方法的實(shí)現(xiàn)直接使用了父類HashMap的方法,LinkedHashMap可以說是HashMap和LinkedList的集合體,既使用了HashMap的數(shù)據(jù)結(jié)構(gòu),又借用了LinkedList雙向鏈表的結(jié)構(gòu)保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構(gòu)造時帶參數(shù),按照訪問次序排序。
構(gòu)造方法
我們發(fā)現(xiàn)除了多了一個變量accessOrder之外,并無不同,此變量到底起了什么作用?
/**
* 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;
通過注釋發(fā)現(xiàn)該變量為true時access-order,即按訪問順序遍歷,如果為false,則表示按插入順序遍歷。默認(rèn)為false
LinkedHashMap 的關(guān)鍵內(nèi)部構(gòu)成
Entry
這個元素實(shí)際上是繼承自HashMap.Node 靜態(tài)內(nèi)部類 ,我們知道HashMap.Node 實(shí)際上是一個單鏈表,因?yàn)樗挥衝ext 節(jié)點(diǎn),但是這里L(fēng)inkedHashMap.Entry保留了HashMap的數(shù)據(jù)結(jié)構(gòu),同時有before, after 兩個節(jié)點(diǎn),一個前驅(qū)節(jié)點(diǎn)一個后繼節(jié)點(diǎn),從而實(shí)現(xiàn)了雙向鏈表
accessOrder
通過注釋發(fā)現(xiàn)該變量為true時access-order,即按訪問順序遍歷,此時你任何一次的操作,包括put、get操作,都會改變map中已有的存儲順序
如果為false,則表示按插入順序遍歷。默認(rèn)為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;
head tail
為了方便操作,加上有是雙向鏈表所有這里定義了兩個特殊的節(jié)點(diǎn),頭結(jié)點(diǎn)和尾部節(jié)點(diǎn)
/**
* 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;
newNode方法
LinkedHashMap重寫了newNode()方法,通過此方法保證了插入的順序性,在此之前我們先看一下HashMap 的newNode()方法
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
然后我們再看一下LinkedHashMap的newNode()方法
這里調(diào)用了一個方法 linkNodeLast(),我們看一下這個方法,但是這和方法不止完成了串聯(lián)后置,也完成了串聯(lián)前置,所以插入的順序性是通過這個方法保證的。
afterNodeAccess 方法
這里將HashMap 中的實(shí)現(xiàn)也帖了出來
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
關(guān)于afterNodeAccess()方法,在HashMap中沒給具體實(shí)現(xiàn),而在LinkedHashMap重寫了,目的是保證操作過的Node節(jié)點(diǎn)永遠(yuǎn)在最后,從而保證讀取的順序性,在調(diào)用put方法和get方法時都會用到
前面說到的newNode()方法中調(diào)用的 linkNodeLast(Entry e)方法和現(xiàn)在的afterNodeAccess(Node e)都是將傳入的Node節(jié)點(diǎn)放到最后,那么它們的使用場景如何呢?
在上一節(jié)講HashMap的put流程,如果在對應(yīng)的hash位置上還沒有元素,那么直接new Node()放到數(shù)組table中,這個時候?qū)?yīng)到LinkedHashMap中,調(diào)用了newNode()方法,就會用到linkNodeLast(),將新node放到最后,
如果對應(yīng)的hash位置上有元素,進(jìn)行元素值的覆蓋時,就會調(diào)用afterNodeAccess(),將原本可能不是最后的node節(jié)點(diǎn)移動到了最后,下面給出了刪減之后的代碼邏輯
下面給出一個例子
運(yùn)行結(jié)果如下
1
2
3
5
afterNodeInsertion 方法
關(guān)于afterNodeAccess()方法,在HashMap中依然沒給具體實(shí)現(xiàn),可以參考afterNodeAccess 中貼出的源代碼
LinkedHashMap中還重寫了afterNodeInsertion(boolean evict)方法,它的目的是移除鏈表中最老的節(jié)點(diǎn)對象,也就是當(dāng)前在頭部的節(jié)點(diǎn)對象,但實(shí)際上在JDK8中不會執(zhí)行,因?yàn)閞emoveEldestEntry方法始終返回false
removeEldestEntry 方法的源代碼如下
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
afterNodeInsertion 方法的意義是什么呢
因?yàn)閞emoveEldestEntry 固定返回false , 那這個方法的意義是什么呢 ?·
afterNodeInsertion方法的evict參數(shù)如果為false,表示哈希表處于創(chuàng)建模式。只有在使用Map集合作為構(gòu)造器參數(shù)創(chuàng)建LinkedHashMap或HashMap時才會為false,使用其他構(gòu)造器創(chuàng)建的LinkedHashMap,之后再調(diào)用put方法,該參數(shù)均為true。
下面給出了單獨(dú)put 的情況
這里是使用Map集合作為構(gòu)造器參數(shù)創(chuàng)建的時的情況
哈哈,其實(shí)到這里還是沒有說這個方法的意義是什么,這里我們回過頭來想一下,看一下HashMap 中有類似的操作嗎,其實(shí)有的,就是這三個方法
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
很明顯這三個方法,都在LinkedHashMap 中被重寫了,所以下面的方法是因?yàn)槭怯蟹祷刂档模运辉谑强辗椒w了,而是一個直接返回false 的方法體了
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
所以這里的意義就是讓你去實(shí)現(xiàn),為什么這么做呢?LinkedHashMap 就是用在記錄順序的場景下,一個典型應(yīng)用就是LRU,也就是主要用在緩存上面,因?yàn)榇藭r的設(shè)計雖然保留了LRU的基本特性,但是整個鏈表的大小是沒有限制的
大小沒有限制的緩存,哈哈 你懂了,后面肯定就是無限的GC了,因?yàn)檫@里都是強(qiáng)引用
實(shí)現(xiàn)一個大小確定的LRU(LinkedHashMap)
如果一個鏈表只能維持10個元素,那么當(dāng)插入了第11個元素時,以如下方式重寫removeEldestEntry的話,那么將會刪除最老的一個元素
程序的運(yùn)行結(jié)果如下
即將執(zhí)行刪除
不二map的大小:10
默認(rèn)map的大小:11
afterNodeRemoval 方法
這個方法和上面的方法一樣,在HashMap 中都是沒有實(shí)現(xiàn)的,但是在LinkedHashMap的實(shí)現(xiàn)如下,其實(shí)這個方法是在刪除節(jié)點(diǎn)后調(diào)用的,可以思考一下為甚么
因?yàn)長inkedHashMap 的雙向鏈表連接了LinkedHashMap中的所有元素,HashMap中的刪除方法可以使沒有考慮這些的,它只考慮了如何存紅黑樹、鏈表中刪除節(jié)點(diǎn),所以它是不維護(hù)雙向鏈表的,所以這里才有了這個方法的實(shí)現(xiàn)
debug 源碼 插入元素的過程
有了HashMap 那一節(jié)和前面的鋪墊,接下來我們看一下完整的插入過程,下面將和HashMap 不一樣的地方標(biāo)注出來
其他細(xì)節(jié)請參考上一篇HashMap,因?yàn)?DRY(don't repeat yourself)
獲取元素的過程
首先 LinkedHashMap 重寫了get 方法,getNode 方法依然使用的實(shí)HashMap 的,當(dāng)元素存在的時候,判斷是否要根據(jù)accessOrder 將元素移動到雙向鏈表的尾部
刪除元素的過程
Hashmap 一節(jié),我們也沒有講remove 方法,所以這里我們來看一下,細(xì)節(jié)的一些東西都寫在注釋里了,首先remove 方法是有返回值的,存在則返回key 對應(yīng)的value 不存在則返回null
matchValue 判斷刪除的時候,是否要求節(jié)點(diǎn)的值是相同的,True 則表示找到key相同的節(jié)點(diǎn)之后,還需要判斷值是相等的才可以使刪除。下面就是判斷條件,可以自己分析一下
(!matchValue || (v = node.value) == value || (value != null && value.equals(v)))
LinkedHashMap重寫了其中的afterNodeRemoval(Node e),該方法在HashMap中沒有具體實(shí)現(xiàn),通過此方法在刪除節(jié)點(diǎn)的時候調(diào)整了雙鏈表的結(jié)構(gòu)。
總結(jié)
LinkedHashMap 的有序性
LinkedHashMap 指的是遍歷的時候的有序性,而有序性是通過雙向鏈表實(shí)現(xiàn)的,真實(shí)的存儲之間是沒有順序的,和Hashmap 一樣
如何實(shí)現(xiàn)一個固定大小的LinkedHashMap
繼承LinkedHashMap實(shí)現(xiàn)removeEldestEntry 方法,當(dāng)插入成功后,判斷是否要刪除最老節(jié)點(diǎn)
四個維護(hù)雙向鏈表的方法
afterNodeAccess(Node<K,V> p)
訪問元素之后維護(hù)
afterNodeInsertion(boolean evict)
插入元素之后維護(hù)
afterNodeRemoval(Node<K,V> p)
刪除元素之后維護(hù)
linkNodeLast
也是插入元素之后維護(hù),但是只用于桶上的第一個節(jié)點(diǎn),后面的節(jié)點(diǎn)都是用afterNodeAccess或者afterNodeInsertion
你覺得LinkedHashMap 還有什么可以改進(jìn)的地方嗎,歡迎討論
和上一節(jié)一樣這里我依然給出這個思考題,雖然我們的說法可能不對,可能我們永遠(yuǎn)也站不到源代碼作者當(dāng)年的高度,但是我們依然積極思考,大膽討論
雖然java 源代碼的山很高,如果你想跨越,至少你得有登山的勇氣,這里我給出自己的一點(diǎn)點(diǎn)愚見,希望各位不吝指教
afterNodeAccess() 方里面有幾處判斷個人覺得是不需要的,具體可以看afterNodeAccess方法里我寫的注釋,歡迎大佬討論,謝謝!
作者:柯廣的網(wǎng)絡(luò)日志
微信公眾號:Java大數(shù)據(jù)與數(shù)據(jù)倉庫