Java集合類:LinkedHashMap
前言
今天繼續學習關於Map家族的另一個類 LinkedHashMap 。先說明一下,LinkedHashMap 是繼承於 HashMap 的,所以本文只針對 LinkedHashMap 的特性學習,跟HashMap 相關的一些特性就不做進一步的解析了,大家有疑惑的可以看之前的博文
深入解析
LinkedHashMap的基本結構
首先,看一下LinkedHashMap類的定義結構:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
它繼承了 HashMap
- HashMap中 ‘’桶“ 的連結串列結點是單向的結點,而LinkedHashMap 中的連結串列結點多出了前後的指向屬性,所以LinkedHashMap 中桶的連結串列是雙向連結串列;
- HashMap中的連結串列只做資料儲存,LinkedHashMap 的連結串列控制儲存順序;
- HashMap桶的連結串列產生是因為產生hash碰撞,所有資料形成連結串列 (紅黑樹) 儲存在一個桶中,LinkedHashMap 中雙向連結串列會串聯所有的資料,也就是說有桶中的資料都是會被這個雙向連結串列管理。
這些區別正是我們專門費勁學習LinkedHashMap 的原因,不然直接用HashMap完了,省事。
下面開始一一深入瞭解。
LinkedHashMap的實體類Entry
LinkedHashMap 類中有專門為雙向連結串列的結點作為載體的實體類Entry,它繼承了HashMap中的Entry,並加入了兩個屬性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); } }
因為這兩個指標,桶中的連結串列才能實現雙向連結串列的功能,將所有的節點已連結串列的形式串聯一起來 ,這是LinkedHashMap 的結構圖 (摘自 Java集合之LinkedHashMap):
新的屬性
LinkedHashMap繼承了HashMap的所有非private屬性,同時也多了幾個新的屬性,分別是
//雙向連結的頭結點,最久的
transient LinkedHashMap.Entry<K,V> head;
//雙向連結的尾結點,最新的
transient LinkedHashMap.Entry<K,V> tail;
//true表示最近最少使用次序(LRU),false表示插入順序
final boolean accessOrder;
看完三個屬性後,我們再來看看預設的構造方法:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
可以看到,LinkedHashMap的構造方法都是預設呼叫了父類的構造方法,並且幾乎都是把屬性accessOrder 賦值為false,除了第五個將其作為引數初始化,也就是說,預設情況下,LinkedHashMap建立物件都是採用插入順序的方式來維持鍵值對的次序的。
插入順序解析
下面通過具體的程式碼來展示 LinkedHashMap 的預設插入順序效果
public class Test {
public static void main(String[] args) {
LinkedHashMap map = new LinkedHashMap<Integer,Integer>();
for (int i = 1; i<=5;i++){
map.put(i,i);
}
System.out.println("正常輸出=="+map.toString());
map.put(6,6);
System.out.println("插入元素=="+map.toString());
}
}
//結果
正常輸出=={1=1, 2=2, 3=3, 4=4, 5=5}
插入元素=={1=1, 2=2, 3=3, 4=4, 5=5, 6=6}
可以看出,當插入新元素時,容器會預設把元素放到最後,這是為什麼呢?
我們點開put原始碼進行檢視,發現直接跳到了HashMap的put方法,也就是說,LinkedHashMap 類中沒有對 put() 做具體的實現,直接複用了父類的方法,這是HashMap中的方法原始碼:
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;
}
在HashMap的原始碼中,初始化容器時呼叫了newNode(),這個方法在 LinkedHashMap 中做了過載,也是其能實現插入順序保證的關鍵,下面看具體的原始碼:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//祕密是這裡初始化的是自己的Entry類,然後呼叫linkNodeLast
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
跟蹤 linkNodeLast 方法,
// link at the end of list,把節點連線到連結串列尾處
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
//把new的Entry給tail
tail = p;
//若沒有last,說明p是第一個節點,head=p
if (last == null)
head = p;
else {
//否則把節點放到連結串列尾處
p.before = last;
last.after = p;
}
}
到這裡就非常清晰了,LinkedHashMap 在插入元素時會呼叫自己過載的newNode() 方法,new一個自己的Entry方法並把節點放到連結串列結尾處,這也是它能實現插入元素順序放置的原因。
LRU演算法的實現
前面說到,LinkedHashMap 建立預設的例項可以實現插入順序的保證效果,它預設初始化的成員變數 accessOrder 的值是false的,如果傳入accessOrder 為true,那麼就啟用LRU演算法,下面給個例子演示下:
import java.util.LinkedHashMap;
public class Test {
public static void main(String[] args) {
Map map = new LinkedHashMap<Integer,Integer>(20,0.75f,true);
for (int i = 1; i<=5;i++){
map.put(i,i);
}
System.out.println("正常輸出=="+map.toString());
map.get(3);
System.out.println("讀取元素=="+map.toString());
map.put(6,6);
System.out.println("插入元素=="+map.toString());
}
}
//輸出結果
正常輸出=={1=1, 2=2, 3=3, 4=4, 5=5}
讀取元素=={1=1, 2=2, 4=4, 5=5, 3=3}
插入元素=={1=1, 2=2, 4=4, 5=5, 3=3, 6=6}
可以看到,當初始化的例項時傳入值為 true 的 accessOrder 時,不管是插入元素還是讀取元素,都是將最近用到的元素放到最後,這是因為 在put 和 get方法中都做了特定的處理。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//為true,呼叫afterNodeAccess方法
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
afterNodeAccess的原始碼解析:
//將最近使用的Node,放在連結串列的最末尾
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
//僅當按照LRU原則且e不在最末尾,才執行修改連結串列,將e移到連結串列最末尾的操作
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//將p的後一個結點置為null,因為執行後p在末尾,後一個結點肯定為null
p.after = null;
//p的前結點不存在,把頭結點設定為a
if (b == null)
head = a;
else
//如果b不為null,那麼b的後節點指向a
b.after = a;
//如果a節點不為空,a的後節點指向b
if (a != null)
a.before = b;
else
//如果a為空,那麼b就是尾節點
last = b;
//尾節點為null,p直接作為頭節點
if (last == null)
head = p;
else {
//否則就把p作為尾節點
p.before = last;
last.after = p;
}
//把p賦值給雙向連結串列的尾節點
tail = p;
++modCount;
}
}
所以,當呼叫這個方法的時候,就會將節點設定到連結串列的尾節點,從而也就達到了LRU的效果。
同理,在put方法中也呼叫了這個方法,不過LinkedHashMap 沒有自己的put方法,直接呼叫的是父類中的方法,在父類的方法中也呼叫了 afterNodeAccess() 方法。
不過,在HashMap中,afterNodeAccess方法並沒有任何實現,LinkedHashMap中過載了該方法,所以,當呼叫put插入元素時,其實也會呼叫LinkedHashMap 的afterNodeAccess方法。
除此之外,LinkedHashMap還有很多過載的方法,限於篇幅就不一一介紹了。
總結
最後說明一下,LinkedHashMap是HashMap的一個子類,其特殊實現的僅僅是儲存了記錄的插入順序,所以在Iterator迭代器遍歷LinkedHashMap時先得到的鍵值是先插入的,然而,由於其儲存沿用了HashMap結構外還多了一個雙向順序連結串列,所以在一般場景下遍歷時會比HashMap慢,此外具備HashMap的所有特性和缺點。
所以,除非是對插入順序讀取比較嚴格的情況,否則不建議用LinkedHashMap,一般情況下,HashMap足以滿足我們的日常使用。