Java集合中的LinkedHashMap使用解析
前言
HashMap是根據(jù)key的hash值進(jìn)行散列存儲(chǔ)的,每個(gè)節(jié)點(diǎn)間是無(wú)序的。而LinkedHashMap可以實(shí)現(xiàn)有序的存儲(chǔ)鍵值對(duì)。
LinkedHashMap是繼承于HashMap的,所以它的很多屬性和方法都是HashMap中的,那么它是怎么實(shí)現(xiàn)有序存儲(chǔ)的呢?
1、屬性
/**
* 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;
/**
* 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;
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);
}
}三個(gè)重要屬性:head、tail、accessOrder,一個(gè)重要結(jié)構(gòu):基礎(chǔ)Entry
從注釋中都可以看出他們的用途,head節(jié)點(diǎn)指向最先存進(jìn)map的鍵值對(duì),tail節(jié)點(diǎn)指向最后存進(jìn)map的鍵值對(duì)
節(jié)點(diǎn)Entry繼承自HashMap的基礎(chǔ)節(jié)點(diǎn)Node,然后又新增了屬性before和after,看名字就知道它們是用來和前后節(jié)點(diǎn)串成雙向鏈表的。
accessOrder決定了鏈表中鍵值對(duì)是依照什么順序來相互串聯(lián)的,true表示鍵值對(duì)的訪問順序,false表示插入順序,默認(rèn)是false。
自此,LinkedHashMap的思想就出來了,本身還是和HashMap一樣都是根據(jù)Hash求索引,散列等等。同時(shí)它自身的每個(gè)節(jié)點(diǎn)又根據(jù)訪問順序或者插入順序串聯(lián)成雙向鏈表,head、tail指向鏈表的頭和尾。
那么它是怎么實(shí)現(xiàn)有序存儲(chǔ)的呢?
2、方法
看下它的方法,發(fā)現(xiàn)它沒有覆寫put方法,只覆寫了get方法

看下HashMap中的put方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}關(guān)鍵就在newNode這個(gè)地方(當(dāng)然newNode在HashMap中不止此處會(huì)用到),LinkedHashMap復(fù)寫了這個(gè)方法。
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;
}
// link at the end of list
//新生成的節(jié)點(diǎn)放到雙向鏈表的隊(duì)尾
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;
}
}所以,每個(gè)新的節(jié)點(diǎn)生成的時(shí)候,都會(huì)把它和前一個(gè)節(jié)點(diǎn)串聯(lián)起來,而且隊(duì)首head指向第一個(gè)生成的節(jié)點(diǎn),隊(duì)尾tail指向最新生成的節(jié)點(diǎn)。
看個(gè)例子
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<>();
map.put("k1", "v1");
map.put("k2", "v2");
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
}執(zhí)行過程如下:

這里只做示意,如果后面hash沖突,需要在桶上加節(jié)點(diǎn)或樹節(jié)點(diǎn),按照順序也是會(huì)和前后的節(jié)點(diǎn)進(jìn)行串聯(lián),和上圖的原理類似。
這是按照插入順序進(jìn)行排序的。如果想按訪問順序進(jìn)行排序,則需要調(diào)用另一個(gè)構(gòu)造函數(shù)。
可以看到,當(dāng)指定訪問順序進(jìn)行排序的時(shí)候,最新被訪問的節(jié)點(diǎn)會(huì)被放到鏈表的末尾。
如何實(shí)現(xiàn)呢?看下HashMap的put方法
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put("k1", "v1");
map.put("k2", "v2");
map.put("k3", "v3");
Set<Map.Entry<String, String>> entries = map.entrySet();
for (Map.Entry entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
map.get("k2");
for (Map.Entry entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
//其他構(gòu)造函數(shù)會(huì)將accessOrder設(shè)置為false
this.accessOrder = accessOrder;
}輸出結(jié)果為:
key:k1, value:v1
key:k2, value:v2
key:k3, value:v3
key:k1, value:v1
key:k3, value:v3
key:k2, value:v2
HashMap中專門定義了三個(gè)給LinkedHashMap覆寫的方法,用于做一些后處理。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }覆寫的這三個(gè)方法都是在進(jìn)行完基本的鍵值對(duì)插入刪除操作后,對(duì)雙向鏈表進(jìn)行調(diào)整。
如afterNodeInsertion方法就是在Hash數(shù)組插入新的Node節(jié)點(diǎn)后,根據(jù)緩存機(jī)制(removeEldestEntry默認(rèn)是緩存機(jī)制不生效,可以覆寫實(shí)現(xiàn)自己的緩存機(jī)制)決定是否刪除雙向鏈表head節(jié)點(diǎn)所指向的節(jié)點(diǎn)。
當(dāng)鍵值對(duì)第一次put進(jìn)來時(shí),如果是生成新的節(jié)點(diǎn)調(diào)用afterNodeInsertion,則此時(shí)和插入順序一致,該節(jié)點(diǎn)位于隊(duì)尾。當(dāng)調(diào)用get方法時(shí)
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)//訪問順序排序會(huì)調(diào)用后處理,將節(jié)點(diǎn)調(diào)整到隊(duì)尾
afterNodeAccess(e);
return e.value;
}此時(shí)會(huì)調(diào)用afterNodeAccess方法,將訪問的這個(gè)節(jié)點(diǎn)調(diào)整到隊(duì)尾。
如果在put時(shí),更新某個(gè)節(jié)點(diǎn)的value,此時(shí)也會(huì)調(diào)用afterNodeAccess,即實(shí)現(xiàn)了一旦某個(gè)節(jié)點(diǎn)被訪問(插入、獲取、更新)時(shí),該節(jié)點(diǎn)被調(diào)整到隊(duì)尾。
刪除時(shí),會(huì)調(diào)用afterNodeRemoval將節(jié)點(diǎn)從雙向鏈表中移除,和之前的原來類似,不再展開。
3、用途
LinkedHashMap能用來做什么呢,由于它自身維護(hù)了一個(gè)雙向鏈表,保證節(jié)點(diǎn)的順序是按照插入或者訪問的順序連接的,所以這種特性在某些場(chǎng)景下就很有用,比如LRU。
什么是LRU,Least Recently Used,最近最少使用。比如一個(gè)Map我最多允許存放10個(gè)鍵值對(duì),那么當(dāng)?shù)?1個(gè)鍵值對(duì)要插入的時(shí)候,我要把已有的10個(gè)中里使用頻率最低的一個(gè)移除。
@Slf4j
public class MapTest {
public static void main(String[] args) {
LRUExample<String, String> lruExample = new LRUExample<>(4);
int i = 0;
for (; i < 4; i++) {
lruExample.put("k" + i, "v" + i);
}
log.info("插入完4個(gè)鍵值對(duì)后");
Set<Map.Entry<String, String>> entries = lruExample.entrySet();
for (Map.Entry<String, String> entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
lruExample.get("k1");
log.info("訪問k1后");
for (Map.Entry<String, String> entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
lruExample.put("k5", "v5");
log.info("插入完第5個(gè)鍵值對(duì)后");
for (Map.Entry<String, String> entry : entries) {
log.info("key:{}, value:{}", entry.getKey(), entry.getValue());
}
}
}
class LRUExample<K, V> extends LinkedHashMap<K, V> {
final int maxSize;
LRUExample(int maxSize) {
super(maxSize, 0.75f, true);
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize;
}
}輸出結(jié)果為:
插入完4個(gè)鍵值對(duì)后
key:k0, value:v0
key:k1, value:v1
key:k2, value:v2
key:k3, value:v3
訪問k0后
key:k1, value:v1
key:k2, value:v2
key:k3, value:v3
key:k0, value:v0
插入完第5個(gè)鍵值對(duì)后
key:k2, value:v2
key:k3, value:v3
key:k0, value:v0
key:k5, value:v5
當(dāng)插入到第5個(gè)時(shí),此時(shí)訪問頻率最低的就是head節(jié)點(diǎn)指向的k1-v1,所以當(dāng)k5-v5插入的時(shí)候,觸發(fā)了緩存清除時(shí),k0-v0被移除,k5-v5加入到隊(duì)尾。
到此這篇關(guān)于Java集合中的LinkedHashMap使用解析的文章就介紹到這了,更多相關(guān)Java集合LinkedHashMap內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java多線程中不同條件下編寫生產(chǎn)消費(fèi)者模型方法介紹
這篇文章主要介紹了Java多線程中不同條件下編寫生產(chǎn)消費(fèi)者模型方法介紹,介紹了生產(chǎn)消費(fèi)者模型,然后分享了相關(guān)代碼示例,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
Java Enum和String及int的相互轉(zhuǎn)化示例
這篇文章主要介紹了Java Enum和String及int的相互轉(zhuǎn)化示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
java使用influxDB數(shù)據(jù)庫(kù)的詳細(xì)代碼
這篇文章主要為大家介紹了java使用influxDB數(shù)據(jù)庫(kù)的詳細(xì)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07
Maven包沖突導(dǎo)致NoSuchMethodError錯(cuò)誤的解決辦法
web 項(xiàng)目 能正常編譯,運(yùn)行時(shí)也正常啟動(dòng),但執(zhí)行到需要調(diào)用 org.codehaus.jackson 包中的某個(gè)方法時(shí),產(chǎn)生運(yùn)行異常,這篇文章主要介紹了Maven包沖突導(dǎo)致NoSuchMethodError錯(cuò)誤的解決辦法,需要的朋友可以參考下2024-05-05
SpringMVC實(shí)現(xiàn)自定義類型轉(zhuǎn)換器
本篇文章主要介紹了SpringMVC實(shí)現(xiàn)自定義類型轉(zhuǎn)換器 ,詳細(xì)的介紹了自定義類型轉(zhuǎn)換器的用法和好處,有興趣的可以了解一下。2017-04-04
http調(diào)用controller方法時(shí)openfeign執(zhí)行流程
這篇文章主要為大家介紹了http調(diào)用controller方法時(shí)openfeign執(zhí)行流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
Git在IDEA中合并多個(gè)commit為一個(gè)
文章介紹了兩種情況下的commit合并方法:未提交到遠(yuǎn)程分支和已經(jīng)提交到遠(yuǎn)程分支,對(duì)于未提交的,通過reset和push操作即可;對(duì)于已提交的,使用rebase操作將多個(gè)commit合并為一個(gè),然后強(qiáng)制push2025-10-10
Java?SE使用for?each循環(huán)遍歷數(shù)組的方法代碼
在Java?SE開發(fā)中,數(shù)組是最常見的數(shù)據(jù)結(jié)構(gòu)之一,Java提供了多種遍歷數(shù)組的方式,其中for循環(huán)是最常用的方式之一,本文將介紹如何使用for?each循環(huán)遍歷數(shù)組,接下來,我們將通過一個(gè)簡(jiǎn)單的代碼示例來展示如何使用for?each循環(huán)遍歷數(shù)組,需要的朋友可以參考下2023-11-11
淺談MyBatis-Plus學(xué)習(xí)之Oracle的主鍵Sequence設(shè)置的方法
這篇文章主要介紹了淺談MyBatis-Plus學(xué)習(xí)之Oracle的主鍵Sequence設(shè)置的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08

