JDK1.8源碼逐字逐句帶你理解LinkedHashMap底層
數據存儲結構
我們已經知道HashMap是以散列表的形式存儲數據的,LinkedHashMap繼承了HashMap,所以LinkedHashMap其實也是散列表的結構,但是“linked”是它對HashMap功能的進一步增強,LinkedHashMap用雙向鏈表的結構,把所有存儲在HashMap中的數據連接起來。有人會說散列表不是已經有了鏈表的存儲結構了嘛,為什麽還要來個雙向鏈表?桶(桶的概念就是數組的一個存儲節點,比如說arr[0]是一個桶,arr[1]也是一個桶)中的鏈表和這個雙向鏈表是兩個概念,以下是我總結的區別:①桶中的鏈表是散列表結構的一部分;而雙向鏈表是LinkedHashMap的額外引入;②桶中的鏈表只做數據存儲,沒有存儲順序的概念;雙向鏈表的核心就是控制數據存儲順序(存儲順序是LinkedHashMap的核心);③桶中的鏈表產生是因為發生了hash碰撞,導致數據散落在一個桶中,用鏈表給予存儲,所以這個鏈表控制了一個桶;雙向鏈表是要串連所有的數據,也就是說有桶中的數據都是會被這個雙向鏈表管理。
所以,我修改了HashMap的圖片,大家參考下:
所以,簡單來說就是LinkedHashMap相比於HashMap來說就是多了這些紅色的雙向鏈表而已。
兩種演示
LinkedHashMap的核心就是存在存儲順序和可以實現LRU算法,所以下面我會用兩個demo先來證明這兩種情況:
①、放入到LinkedHashMap是有順序的,會按照你放入的順序存儲:
package com.brickworkers;
import java.util.LinkedHashMap;
/**
* @author Brickworker
* Date:2017年4月12日下午12:46:25
* 關於類LinkedHashMapTest.java的描述:jdk1.8逐字逐句帶你理解linkedHashMap
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();
for (int i = 0; i < 10; i++) {//按順序放入1~9
map.put(i, i);
}
System.out.println("原數據:"+map.toString());
map.get(3);
System.out.println("查詢存在的某一個:"+map.toString());
map.put(4, 4);
System.out.println("插入已存在的某一個:"+map.toString()); //直接調用已存在的toString方法,不然自己需要用叠代器實現
map.put(10, 10);
System.out.println("插入一個原本沒存在的:"+map.toString());
}
//輸出結果
// 原數據:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 查詢存在的某一個:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 插入已存在的某一個:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 插入一個原本沒存在的:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
觀察以上代碼,其實它是符合先進先出的規則的,不管你怎麽查詢插入已存在的數據,不會對排序造成影響,如果有新插入的數據將會放在最尾部。
②啟用LRU規則的LinkedHashMap,啟動這個規則需要在構造LinkedHashMap的時候,調用三個參數的構造器,這個構造器源碼如下:
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the
* specified initial capacity, load factor and ordering mode.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param accessOrder the ordering mode - <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;//是否開啟LRU規則
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
第三個參數accessOrder就是用於控制LRU規則的。
如下就是我寫的demo:
package com.brickworkers;
import java.util.LinkedHashMap;
/**
* @author Brickworker
* Date:2017年4月12日下午12:46:25
* 關於類LinkedHashMapTest.java的描述:jdk1.8逐字逐句帶你理解linkedHashMap
* Copyright (c) 2017, brcikworker All Rights Reserved.
*/
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(20, 0.75f, true);
for (int i = 0; i < 10; i++) {//按順序放入1~9
map.put(i, i);
}
System.out.println("原數據:"+map.toString());
map.get(3);
System.out.println("查詢存在的某一個:"+map.toString());
map.put(4, 4);
System.out.println("插入已存在的某一個:"+map.toString()); //直接調用已存在的toString方法,不然自己需要用叠代器實現
map.put(10, 10);
System.out.println("插入一個原本沒存在的:"+map.toString());
}
//輸出結果
// 原數據:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
// 查詢存在的某一個:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3} //被訪問(get)的3放到了最後面
// 插入已存在的某一個:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4}//被訪問(put)的4放到了最後面
// 插入一個原本沒存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4, 10=10}//新增一個放到最後面
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
從上面可以看出,每當我get或者put一個已存在的數據,就會把這個數據放到雙向鏈表的尾部,put一個新的數據也會放到雙向鏈表的尾部。
逐字逐句底層源碼
接下來我們通過源碼深入學習LinkedHash,同時解答上述出現的有趣的事情。
①分析LinkedHashMap的類名和繼承關系:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
- 1
- 2
- 3
- 4
從這裏我們可以看出LinkedHashMap是繼承了HashMap並實現了Map接口的,所以它和HashMap的關系肯定不一般。
②分析LinkedHashMap的構造函數:
//1
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//2
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//3
public LinkedHashMap() {
super();
accessOrder = false;
}
//4
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
//5
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
它具有5個構造函數,可以設置容量和加載因子,且在默認情況下是不開啟LRU規則的。同時它還以用具有繼承K,V關系的map進行初始化。
③分析雙向鏈表
/**
* HashMap.Node subclass for normal LinkedHashMap entries.
*/
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);
}
}
/**
* 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;//雙向列表尾節點(最新)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
用一個靜態內部類Entry表示雙向鏈表,實現了HashMap中的Node內部類。before和after表示前後指針。我們在使用LinkedHashMap有序就是因此產生。
④分析LRU規則實現,最近訪問放在雙向鏈表最後面
void afterNodeAccess(Node<K,V> e) { // 把當前節點e放到雙向鏈表尾部
LinkedHashMap.Entry<K,V> last;
//accessOrder就是我們前面說的LRU控制,當它為true,同時e對象不是尾節點(如果訪問尾節點就不需要設置,該方法就是把節點放置到尾節點)
if (accessOrder && (last = tail) != e) {
//用a和b分別記錄該節點前面和後面的節點
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//釋放當前節點與後節點的關系
p.after = null;
//如果當前節點的前節點是空,
if (b == null)
//那麽頭節點就設置為a
head = a;
else
//如果b不為null,那麽b的後節點指向a
b.after = a;
//如果a節點不為空
if (a != null)
//a的後節點指向b
a.before = b;
else
//如果a為空,那麽b就是尾節點
last = b;
//如果尾節點為空
if (last == null)
//那麽p為頭節點
head = p;
else {
//否則就把p放到雙向鏈表最尾處
p.before = last;
last.after = p;
}
//設置尾節點為P
tail = p;
//LinkedHashMap對象操作次數+1
++modCount;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
afterNodeAccess方法就是如何支持LRU規則的,如果在accessOrder為true的時候,節點調用這個函數,就會把該節點放置到最後面。put,get等都會調用這個函數來調整順序,我手畫了一張圖來表示這個函數幹了些什麽:
我們看看get方法中是否調用了此函數,以下是LinkedHashMap重寫了HashMap的get方法:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)//如果啟用了LRU規則
afterNodeAccess(e);//那麽把該節點移到雙向鏈表最後面
return e.value;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
那麽有些小夥伴就問了,那麽put方法裏面調用了嘛?肯定調用了,但是你不一定找得到,因為LinkedHashMap壓根沒有重寫put方法,每次用LinkedHashMap的put方法的時候,其實你調用的都是HashMap的put方法。那為什麽它也會執行afterNodeAccess()方法呢,因為這個方法HashMap就是存在的,但是沒有實現,LinkedHashMap重寫了afterNodeAccess()這個方法。下面給出HashMap的put局部方法:
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);//把當前節點移動到雙向鏈表最尾處
return oldValue;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
其實這個方法在很多的調用都有,這裏就不一一解釋了。同時,LinkedHashMap對於紅黑樹的節點移動處理也有專門的方法,這裏就不再深入講解了,方式也是差不多。
⑤分析一個LinkedHashMap自帶的移除最頭(最老)數據的方法
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
- 1
- 2
- 3
- 4
LinkedHashMap有一個自帶的移除最老數據的方法,但是這個方法永遠是返回false,但是如果我們要實現,可以在繼承的時候重寫這個方法,給定一個條件就可以控制存儲在LinkedHashMap中的最老數據何時刪除。具體的在我以前博文多種方式實現緩存機制中有寫過。觸發這個刪除機制,一般是在PUT一個數據進入的時候,但是LinkedHashMap並沒有重寫Put方法如何實現呢?在LinekdHashMap中,這個方法被包含在afterNodeInsertion()方法之中,而這個方法是重寫了HashMap的,但是HashMap中並沒有去實現它,所以在put的時候就會觸發刪除這個機制。
尾記
技術是不斷前進的,或許在JDK1.8的時候我寫的這些是有用的,但是下一個版本出來就不一定了。比如說前面幾個版本中,LinkedHashMap是一個循環的雙向鏈表,而且需要用init()方法進行初始化,但是後來都被移除了,以下是部分原本的linkedHashMap源碼:
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header; //循環的雙向鏈表
}
- 1
- 2
- 3
- 4
所以,在日常的學習中,尤其是技術,要與時俱進,在查詢某個技術點的時候,千萬要註意版本號,不一樣的版本之間可能是天差地別的。
JDK1.8源碼逐字逐句帶你理解LinkedHashMap底層