LinkedHashMap源碼詳解
LinkedHashMap是基於HashMap實現的,如果對HashMap不瞭解,請先學習HashMap:http://blog.csdn.net/luanmousheng/article/details/75195809
HashMap的無序性和LinkedHashMap的有序性
前面介紹的HashMap查找效率很高,但是也有一個缺點,即遍歷HashMap是無序的。LinkedHashMap顧名思義,是鏈表和哈希表的結合,鏈表具有天然的有序性,這裏的有序不是按照節點大小排序,而是按照節點的插入順序排序或者節點的訪問順序排序。
爲了比較HashMap和LinkedHashMap的有序性,首先觀察HashMap的遍歷結果:
Map<String, String> map2 = new HashMap();
map2.put("name", "jack");
map2.put("age", "23");
map2.put("job", "student");
map2.put("home", "china");
Iterator<Map.Entry<String, String>> it2 = map2.entrySet().iterator();
while(it2.hasNext()) {
System.out.println(it2.next().getKey());
}
這段代碼段輸出:
home
age
name
job
可以看到,遍歷HashMap時的輸出和輸入時的順序沒有關係。
再觀察LinkedHashMap的遍歷結果:
LinkedHashMap<String, String> map = new LinkedHashMap();
map.put("name", "jack");
map.put("home", "china");
map.put("age", "23");
map.put("job", "student");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while(it.hasNext()) {
System.out.println(it.next().getKey());
}
這段代碼輸出:
name
home
age
job
可以看到,默認情況下,遍歷LinkedHashMap時的輸出和輸入的順序是一致的。因此我們說LinkedHashMap是可以保證有序性的。
我們說默認情況下,遍歷LinkedHashMap時的輸出和輸入的順序是一致的,LinkedHashMap還可以根據節點的訪問順序進行排序,即最新訪問的節點放在最前面。LinkedHashMap提供了accessOrder字段,這個字段可以指示LinkedHashMap是否按照訪問時間進行排序,通過LinkedHashMap另一個帶參數的構造函數可以創建一個按照訪問時間排序的哈希表,看下面的例子:
LinkedHashMap<String, String> map = new LinkedHashMap(10, 0.75F, true);
map.put("name", "jack");
map.put("home", "china");
map.put("age", "23");
map.put("job", "student");
//訪問了鍵"home"的節點
map.get("home");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while(it.hasNext()) {
System.out.println(it.next().getKey());
}
將4個鍵值對添加到LinkedHashMap後,訪問了鍵”home”的節點,這段代碼的輸出爲:
name
age
job
home
可以看到,鍵”home”被我們訪問後,放到了最後一個位置(最新的位置),這就是LinkedHashMap按照訪問先後順序的有序性。
好了,對於LinkedHashMap有了基本的認識後,下面將基於源碼,詳細介紹LinkedHashMap的原理。
LinkedHashMap原理
LinkedHashMap的存儲還是通過HashMap實現的,但是它和HashMap最大的區別在於LinkedHashMap維護了一個雙向鏈表,這個雙向鏈表按照節點的插入順序保存節點、或者按照節點的訪問先後順序保存節點。
先看下LinkedHashMap的聲明:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
//……
private static class Entry<K,V> extends HashMap.Entry<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
//……
}
}
LinkedHashMap繼承了HashMap,內部靜態類Entry繼承了HashMap的Entry,但是它和HashMap.Entry不同的是,LinkedHashMap.Entry多了兩個字段:before和after,before表示在本節點之前添加到LinkedHashMap的那個節點,after表示在本節點之後添加到LinkedHashMap的那個節點,這裏的之前和之後指時間上的先後順序。
有了對LinkedHashMap.Entry的瞭解,通過示意圖學習LinkedHashMap的工作原理。
圖1 LinkedHashMap初始狀態
LinkedHashMap的初始狀態包括一個HashMap和一個只有頭節點的雙向鏈表。
接着插入鍵K1:
圖2 插入鍵K1的狀態
接着插入鍵K2:
圖3 插入鍵K2的狀態
通過以上三個示意圖,基本上可以理解LinkedHashMap的工作原理,示意圖的左邊部分是HashMap,右邊部分是雙向鏈表,這個雙向鏈表記錄了鍵的添加順序。注意,這裏我們將HashMap中的鍵和鏈表中的鍵分開表示,其實它們是同一個節點,分開後利於觀察,否則很多指針糾纏在一起,示意圖會顯得很混亂。
以上三個示意圖都是基於插入順序排序的,假設當前LinkedHashMap的狀態如圖3,並且我們創建該哈希表時候指定了按訪問時間排序,當我們在圖3的基礎上分別添加K3、訪問K2後的狀態爲:
圖4 分別添加K3、訪問K2後的狀態
在圖3的基礎上,添加K3、訪問K2後將K2移到了鏈表的末尾。
將LinkedHashMap的accessOrder字段設置爲true後,每次訪問哈希表中的節點都將該節點移到鏈表的末尾,表示該節點是最新訪問的節點。
好了,我們通過幾個示意圖已經瞭解了LinkedHashMap的工作原理,接着學習LinkedHashMap的源碼。
LinkedHashMap源碼解析
我們使用哈希表是爲了往哈希表中添加鍵值對,那我們就從最基本的方法put說起。
put方法
圖5 LinkedHashMap中搜索put方法
在idea中搜索LinkedHashMap的put方法,驚奇的發現,LinkedHashMap中並沒有定義put方法,相反,idea向我們推薦了很多put的實現,顯然我們應該去看HashMap的put實現,也就是說,LinkedHashMap的方法即是HashMap的put方法,當我們調用LinkedHashMap的put方法實際上調用的是父類HashMap的put方法。
看下HashMap的put實現:
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value);
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> 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;
//1:HashMap中是個空實現,子類LinkedHashMap的Entry實現了該方法
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//2:子類LinkedHashMap重寫了該方法
addEntry(hash, key, value, i);
return null;
}
上面代碼1處和2處正是LinkedHashMap的不同之處,LinkedHashMap重寫了HashMap.Entry的recordAccess方法和HashMap的addEntry方法。其中recordAccess方法在父類HashMap的Entry是個空實現,子類LinkedHashMap.Entry重寫該方法是爲了記錄節點訪問的先後順序。
是時候介紹LinkedHashMap的Entry類了:
private static class Entry<K,V> extends HashMap.Entry<K,V> {
//before節點在當前節點之前插入
//after節點在當前節點之後插入
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
//直接調用了父類的構造函數
super(hash, key, value, next);
}
//將當前節點從該雙向鏈表中刪除
private void remove() {
before.after = after;
after.before = before;
}
//將當前節點插入指定節點的前面
//其實就是改變鏈表的指針指向
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
//這是LinkedHashMap與HashMap重要的不同之處
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
//若是節點訪問先後順序的規則
lm.modCount++;
//先把當前節點刪除,然後把該節點添加到頭節點的前面,也就是鏈表的末尾,參考示意圖2和圖3
remove();
addBefore(lm.header);
}
}
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
這段代碼最重要之處在於對accessOrder的判斷,若是基於訪問順序,每次訪問節點後需要將該節點移到鏈表的末尾處,否則recordAccess什麼也不做。其他的代碼就是對雙向鏈表指針指向的改變,參考圖2、3、4。
accessOrder的意義在於,每次訪問一個節點都將該節點移到鏈表的末尾,表示這個節點是最新訪問的節點,這在很多場景下都很有用處,比如LRU算法,排在鏈表末尾部分的節點都是最近使用過的節點,那麼排在前面部分的節點就可能長期都沒有被訪問過,此時系統可以將這些節點刪除以增加可用內存。
好了,現在繼續看put方法調用的addEntry方法。子類LinkedHashMap重寫了HashMap的addEntry方法,看下LinkedHashMap的addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//調用了父類HashMap的addEntry方法
super.addEntry(hash, key, value, bucketIndex);
// 如果需要的話刪除在鏈表中最久的節點
Entry<K,V> eldest = header.after;
//removeEldestEntry是個protected方法。
//LinkedHashMap中該方法返回false,也就是不會刪除在鏈表中最久的節點。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
}
}
LinkedHashMap的addEntry方法首先調用了父類的addEntry方法,注意,子類可以重寫removeEldestEntry方法並返回true,刪除在鏈表中最久的節點。
到此爲止,似乎並沒有找到令我們多麼興奮的事,畢竟LinkedHashMap的removeEldestEntry返回了false,也就是LinkedHashMap只是調用了父類HashMap的addEntry方法,並沒有做其他的事。繼續深究HashMap的addEntry方法,能發現一些不同的地方:
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
//LinkedHashMap重寫了該方法
createEntry(hash, key, value, bucketIndex);
}
LinkedHashMap重寫了父類的createEntry方法,讓我們繼續看LinkedHashMap的createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
//1:將節點e加入到header的前面,也就是鏈表的末尾
e.addBefore(header);
size++;
}
比較HashMap和LinkedHashMap,我們發現LinkedHashMap在1處有不同,其他地方都完全相同。
LinkedHashMap創建結點並將該節點添加到HashMap後,該節點會被鏈接到頭結點header鏈表的末尾,在這裏實現了LinkedHashMap插入的有序性。
構造函數
到這裏解釋完了put方法,接下來看下LinkedHashMap的構造方法,LinkedHashMap有很多重載的構造方法,原理大致大同,這裏介紹其中一個構造方法來講解:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
這個帶3個參數的構造函數其中initialCapacity和loadFactor決定了哈希表的初始容量和加載因子,accessOrder決定了LinkedHashMap的排序規則,如果accessOrder=false,則按插入順序排序,否則按訪問順序排序。
該構造函數調用了父類的構造函數,看下HashMap中的構造函數:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
threshold = initialCapacity;
//HashMap中該方法是個空方法,子類LinkedHashMap重寫了該方法
init();
}
父類HashMap中的init方法是個空實現,子類LinkedHashMap重寫了該方法,看下LinkedHashMap中init方法的實現:
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
該方法創建了一個只帶頭節點的鏈表,參考圖1。
LinkedHashMap的所有構造函數都會調用父類HashMap的構造函數,HashMap的構造函數都會調用init方法,即我們創建LinkedHashMap時總會調用init方法創建一個只帶頭節點的鏈表。
get方法
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
//看這裏,LinkedHashMap的不同之處
e.recordAccess(this);
return e.value;
}
這裏需要注意e.recordAccess的調用,這個調用之前我們已經分析過,每次訪問一個節點的時候都要根據accessOrder是否爲true,決定是否將該節點移到鏈表末尾,表示該節點是最近訪問的節點,實現按照訪問順序的有序性。
containsValue方法
public boolean containsValue(Object value) {
if (value==null) {
//在鏈表中查找值爲null的節點
for (Entry e = header.after; e != header; e = e.after)
if (e.value==null)
return true;
} else {
//在鏈表中查找值爲value的節點
for (Entry e = header.after; e != header; e = e.after)
if (value.equals(e.value))
return true;
}
return false;
}
LinkedHashMap的containsValue方法利用了該哈希表的鏈表特性,在鏈表中查找是否存在對應的值。
transfer方法
void transfer(HashMap.Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
//利用LinkedHashMap的鏈表特性將節點放到新的哈希表
for (Entry<K,V> e = header.after; e != header; e = e.after) {
if (rehash)
e.hash = (e.key == null) ? 0 : hash(e.key);
int index = indexFor(e.hash, newCapacity);
e.next = newTable[index];
newTable[index] = e;
}
}
該方法將原來哈希表中的Entry轉移到新的哈希表,通過遍歷鏈表將節點放到新的哈希表中。
迭代器
LinkedHashMap的迭代器是通過它的內部類實現的,其中最主要的類是LinkedHashIterator,該類是個抽象類,提供了基本的迭代方法。Entry迭代器、Key迭代器和Value迭代器都是通過繼承該抽象類實現的。首先看下LinkedHashIterator:
private abstract class LinkedHashIterator<T> implements Iterator<T> {
//從鏈表頭結點的後一個節點開始遍歷
Entry<K,V> nextEntry = header.after;
//保存了最近訪問到的節點
Entry<K,V> lastReturned = null;
//該字段用於快速失敗機制,當迭代器發現這兩個值不相等,說明有其他線程改變了
//該哈希表,拋出ConcurrentModificationException
int expectedModCount = modCount;
//如果下個節點是頭節點,說明遍歷結束
public boolean hasNext() {
return nextEntry != header;
}
public void remove() {
//最近返回的節點爲null,不能刪除
if (lastReturned == null)
throw new IllegalStateException();
//快速失敗機制,拋出併發修改異常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
LinkedHashMap.this.remove(lastReturned.key);
//每迭代一次只能刪除一次,不能迭代一次刪除多次
lastReturned = null;
expectedModCount = modCount;
}
Entry<K,V> nextEntry() {
//快速失敗機制
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//不存在下個節點
if (nextEntry == header)
throw new NoSuchElementException();
//保存最近訪問的節點
Entry<K,V> e = lastReturned = nextEntry;
nextEntry = e.after;
return e;
}
}
LinkedHashIterator還是比較好理解的,和HashMap的迭代器類似,直接看源碼註釋好了。
EntryIterator、KeyIterator和ValueIterator都是通過繼承LinkedHashIterator實現的:
private class KeyIterator extends LinkedHashIterator<K> {
//鍵的迭代器
public K next() { return nextEntry().getKey(); }
}
private class ValueIterator extends LinkedHashIterator<V> {
//值的迭代器
public V next() { return nextEntry().value; }
}
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
//Entry的迭代器
public Map.Entry<K,V> next() { return nextEntry(); }
}
總結
好了,LinkedHashMap的源碼就解釋完了,做下總結:
- LinkedHashMap是雙向鏈表和HashMap的完美結合。
- LinkedHashMap是有序的,默認通過插入順序排序,也可以通過構造函數的參數accessOrder指定通過訪問順序排序。
- 只要理解了HashMap的工作原理,就很容易理解LinkedHashMap,它只是在HashMap的基礎上將各個Entry節點通過雙鏈錶鏈接起來實現有序性。