java-原始碼解讀-jdk1.6-LinkedHashMap與HashMap
摘要:
HashMap和雙向連結串列合二為一即是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap,因此更準確地說,它是一個將所有Entry節點鏈入一個雙向連結串列的HashMap。由於LinkedHashMap是HashMap的子類,所以LinkedHashMap自然會擁有HashMap的所有特性。比如,LinkedHashMap的元素存取過程基本與HashMap基本類似,只是在細節實現上稍有不同。當然,這是由LinkedHashMap本身的特性所決定的,因為它額外維護了一個雙向連結串列用於保持迭代順序。此外,LinkedHashMap可以很好的支援LRU演算法,筆者在第七節便在LinkedHashMap的基礎上實現了一個能夠很好支援LRU的結構。
友情提示:
本文所有關於 LinkedHashMap 的原始碼都是基於 JDK 1.6 的,不同 JDK 版本之間也許會有些許差異,但不影響我們對 LinkedHashMap 的資料結構、原理等整體的把握和了解。
由於 LinkedHashMap 是 HashMap 的子類,所以其具有HashMap的所有特性,這一點在原始碼共用上體現的尤為突出。因此,讀者在閱讀本文之前,最好對 HashMap 有一個較為深入的瞭解和回顧,否則很可能會導致事倍功半。
一. LinkedHashMap 概述
HashMap 是 Java Collection Framework 的重要成員,也是Map族(如下圖所示)中我們最為常用的一種。不過遺憾的是,HashMap是無序的,也就是說,迭代HashMap所得到的元素順序並不是它們最初放置到HashMap的順序。HashMap的這一缺點往往會造成諸多不便,因為在有些場景中,我們確需要用到一個可以保持插入順序的Map。
本質上,HashMap和雙向連結串列合二為一即是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap,因此更準確地說,它是一個將所有Entry節點鏈入一個雙向連結串列雙向連結串列的HashMap。在LinkedHashMapMap中,所有put進來的Entry都儲存在如下面第一個圖所示的雜湊表中,但由於它又額外定義了一個以head為頭結點的雙向連結串列(如下面第二個圖所示),因此對於每次put進來Entry,除了將其儲存到雜湊表中對應的位置上之外,還會將其插入到雙向連結串列的尾部。
更直觀地,下圖很好地還原了LinkedHashMap的原貌:HashMap和雙向連結串列的密切配合和分工合作造就了LinkedHashMap。特別需要注意的是,next用於維護HashMap各個桶中的Entry鏈,before、after用於維護LinkedHashMap的雙向連結串列,雖然它們的作用物件都是Entry,但是各自分離,是兩碼事兒。
其中,HashMap與LinkedHashMap的Entry結構示意圖如下圖所示:
特別地,由於LinkedHashMap是HashMap的子類,所以LinkedHashMap自然會擁有HashMap的所有特性。比如,LinkedHashMap也最多隻允許一條Entry的鍵為Null(多條會覆蓋),但允許多條Entry的值為Null。此外,LinkedHashMap 也是 Map 的一個非同步的實現。此外,LinkedHashMap還可以用來實現LRU (Least recently used, 最近最少使用)演算法,這個問題會在下文的特別談到。
二. LinkedHashMap 在 JDK 中的定義
1、類結構定義
LinkedHashMap繼承於HashMap,其在JDK中的定義為:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V> {
...
}
2、成員變數定義
與HashMap相比,LinkedHashMap增加了兩個屬性用於保證迭代順序,分別是 雙向連結串列頭結點header 和 標誌位accessOrder (值為true時,表示按照訪問順序迭代;值為false時,表示按照插入順序迭代)。
/**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header; // 雙向連結串列的表頭元素
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
private final boolean accessOrder; //true表示按照訪問順序迭代,false時表示按照插入順序
3、成員方法定義
從下圖我們可以看出,LinkedHashMap中並增加沒有額外方法。也就是說,LinkedHashMap與HashMap在操作上大致相同,只是在實現細節上略有不同罷了。
4、基本元素 Entry
LinkedHashMap採用的hash演算法和HashMap相同,但是它重新定義了Entry。LinkedHashMap中的Entry增加了兩個指標 before 和 after,它們分別用於維護雙向連結列表。特別需要注意的是,next用於維護HashMap各個桶中Entry的連線順序,before、after用於維護Entry插入的先後順序的,原始碼如下:
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
...
}
形象地,HashMap與LinkedHashMap的Entry結構示意圖如下圖所示:
三. LinkedHashMap 的建構函式
LinkedHashMap 一共提供了五個建構函式,它們都是在HashMap的建構函式的基礎上實現的,分別如下:
1、LinkedHashMap()
該建構函式意在構造一個具有 預設初始容量 (16)和預設負載因子(0.75)的空 LinkedHashMap,是 Java Collection Framework 規範推薦提供的,其原始碼如下:
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the default initial capacity (16) and load factor (0.75).
*/
public LinkedHashMap() {
super(); // 呼叫HashMap對應的建構函式
accessOrder = false; // 迭代順序的預設值
}
2、LinkedHashMap(int initialCapacity, float loadFactor)
該建構函式意在構造一個指定初始容量和指定負載因子的空 LinkedHashMap,其原始碼如下:
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor); // 呼叫HashMap對應的建構函式
accessOrder = false; // 迭代順序的預設值
}
3、LinkedHashMap(int initialCapacity)
該建構函式意在構造一個指定初始容量和預設負載因子 (0.75)的空 LinkedHashMap,其原始碼如下:
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and a default load factor (0.75).
*
* @param initialCapacity the initial capacity
* @throws IllegalArgumentException if the initial capacity is negative
*/
public LinkedHashMap(int initialCapacity) {
super(initialCapacity); // 呼叫HashMap對應的建構函式
accessOrder = false; // 迭代順序的預設值
}
4、LinkedHashMap(Map<? extends K, ? extends V> m)
該建構函式意在構造一個與指定 Map 具有相同對映的 LinkedHashMap,其 初始容量不小於 16 (具體依賴於指定Map的大小),負載因子是 0.75,是 Java Collection Framework 規範推薦提供的,其原始碼如下:
/**
* Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
* the same mappings as the specified map. The <tt>LinkedHashMap</tt>
* instance is created with a default load factor (0.75) and an initial
* capacity sufficient to hold the mappings in the specified map.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m); // 呼叫HashMap對應的建構函式
accessOrder = false; // 迭代順序的預設值
}
5、LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
該建構函式意在構造一個指定初始容量和指定負載因子的具有指定迭代順序的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); // 呼叫HashMap對應的建構函式
this.accessOrder = accessOrder; // 迭代順序的預設值
}
正如我們在《Map 綜述(一):徹頭徹尾理解 HashMap》一文中提到的那樣,初始容量 和負載因子是影響HashMap效能的兩個重要引數。同樣地,它們也是影響LinkedHashMap效能的兩個重要引數。此外,LinkedHashMap 增加了雙向連結串列頭結點 header和標誌位 accessOrder兩個屬性用於保證迭代順序。
6、init 方法
從上面的五種建構函式我們可以看出,無論採用何種方式建立LinkedHashMap,其都會呼叫HashMap相應的建構函式。事實上,不管呼叫HashMap的哪個建構函式,HashMap的建構函式都會在最後呼叫一個init()方法進行初始化,只不過這個方法在HashMap中是一個空實現,而在LinkedHashMap中重寫了它用於初始化它所維護的雙向連結串列。例如,HashMap的引數為空的建構函式以及init方法的原始碼如下:
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
/**
* Initialization hook for subclasses. This method is called
* in all constructors and pseudo-constructors (clone, readObject)
* after HashMap has been initialized but before any entries have
* been inserted. (In the absence of this method, readObject would
* require explicit knowledge of subclasses.)
*/
void init() {
}
在LinkedHashMap中,它重寫了init方法以便初始化雙向列表,原始碼如下:
/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
header = new Entry<K,V>(-1, null, null, null);
header.before = header.after = header;
}
因此,我們在建立LinkedHashMap的同時就會不知不覺地對雙向連結串列進行初始化。
四. LinkedHashMap 的資料結構
本質上,LinkedHashMap = HashMap + 雙向連結串列,也就是說,HashMap和雙向連結串列合二為一即是LinkedHashMap。也可以這樣理解,LinkedHashMap 在不對HashMap做任何改變的基礎上,給HashMap的任意兩個節點間加了兩條連線(before指標和after指標),使這些節點形成一個雙向連結串列。在LinkedHashMapMap中,所有put進來的Entry都儲存在HashMap中,但由於它又額外定義了一個以head為頭結點的空的雙向連結串列,因此對於每次put進來Entry還會將其插入到雙向連結串列的尾部。
五. LinkedHashMap 的快速存取
我們知道,在HashMap中最常用的兩個操作就是:put(Key,Value) 和 get(Key)。同樣地,在 LinkedHashMap 中最常用的也是這兩個操作。對於put(Key,Value)方法而言,LinkedHashMap完全繼承了HashMap的 put(Key,Value) 方法,只是對put(Key,Value)方法所呼叫的recordAccess方法和addEntry方法進行了重寫;對於get(Key)方法而言,LinkedHashMap則直接對它進行了重寫。下面我們結合JDK原始碼看 LinkedHashMap 的存取實現。
1、LinkedHashMap 的儲存實現 : put(key, vlaue)
上面談到,LinkedHashMap沒有對 put(key,vlaue) 方法進行任何直接的修改,完全繼承了HashMap的 put(Key,Value) 方法,其原始碼如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or null if there was no mapping for key.
* Note that a null return can also indicate that the map previously associated null with key.
*/
public V put(K key, V value) {
//當key為null時,呼叫putForNullKey方法,並將該鍵值對儲存到table的第一個位置
if (key == null)
return putForNullKey(value);
//根據key的hashCode計算hash值
int hash = hash(key.hashCode());
//計算該鍵值對在陣列中的儲存位置(哪個桶)
int i = indexFor(hash, table.length);
//在table的第i個桶上進行迭代,尋找 key 儲存的位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
//判斷該條鏈上是否存在hash值相同且key值相等的對映,若存在,則直接覆蓋 value,並返回舊value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); // LinkedHashMap重寫了Entry中的recordAccess方法--- (1)
return oldValue; // 返回舊值
}
}
modCount++; //修改次數增加1,快速失敗機制
//原Map中無該對映,將該新增至該鏈的鏈頭
addEntry(hash, key, value, i); // LinkedHashMap重寫了HashMap中的createEntry方法 ---- (2)
return null;
}
上述原始碼反映了LinkedHashMap與HashMap儲存資料的過程。特別地,在LinkedHashMap中,它對addEntry方法和Entry的recordAccess方法進行了重寫。下面我們對比地看一下LinkedHashMap 和HashMap的addEntry方法的具體實現:
/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*
* LinkedHashMap中的addEntry方法
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//建立新的Entry,並插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex); // 重寫了HashMap中的createEntry方法
//雙向連結串列的第一個有效節點(header後的那個節點)為最近最少使用的節點,這是用來支援LRU演算法的
Entry<K,V> eldest = header.after;
//如果有必要,則刪除掉該近期最少使用的節點,
//這要看對removeEldestEntry的覆寫,由於預設為false,因此預設是不做任何處理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
//擴容到原來的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
-------------------------------我是分割線------------------------------------
/**
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*
* HashMap中的addEntry方法
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//獲取bucketIndex處的Entry
Entry<K,V> e = table[bucketIndex];
//將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//若HashMap中元素的個數超過極限了,則容量擴大兩倍
if (size++ >= threshold)
resize(2 * table.length);
}
由於LinkedHashMap本身維護了插入的先後順序,因此其可以用來做快取,14~19行的操作就是用來支援LRU演算法的,這裡暫時不用去關心它。此外,在LinkedHashMap的addEntry方法中,它重寫了HashMap中的createEntry方法,我們接著看一下createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) {
// 向雜湊表中插入Entry,這點與HashMap中相同
//建立新的Entry並將其鏈入到陣列對應桶的連結串列的頭結點處,
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
//在每次向雜湊表插入Entry的同時,都會將其插入到雙向連結串列的尾部,
//這樣就按照Entry插入LinkedHashMap的先後順序來迭代元素(LinkedHashMap根據雙向連結串列重寫了迭代器)
//同時,新put進來的Entry是最近訪問的Entry,把其放在連結串列末尾 ,也符合LRU演算法的實現
e.addBefore(header);
size++;
}
由以上原始碼我們可以知道,在LinkedHashMap中向雜湊表中插入新Entry的同時,還會通過Entry的addBefore方法將其鏈入到雙向連結串列中。其中,addBefore方法本質上是一個雙向連結串列的插入操作,其原始碼如下:
//在雙向連結串列中,將當前的Entry插入到existingEntry(header)的前面
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
到此為止,我們分析了在LinkedHashMap中put一條鍵值對的完整過程。總的來說,相比HashMap而言,LinkedHashMap在向雜湊表新增一個鍵值對的同時,也會將其鏈入到它所維護的雙向連結串列中,以便設定迭代順序。
2、LinkedHashMap 的擴容操作 : resize()
在HashMap中,我們知道隨著HashMap中元素的數量越來越多,發生碰撞的概率將越來越大,所產生的子鏈長度就會越來越長,這樣勢必會影響HashMap的存取速度。為了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理,該臨界點就是HashMap中元素的數量在數值上等於threshold(table陣列長度*載入因子)。但是,不得不說,擴容是一個非常耗時的過程,因為它需要重新計算這些元素在新table陣列中的位置並進行復制處理。所以,如果我們能夠提前預知HashMap 中元素的個數,那麼在構造HashMap時預設元素的個數能夠有效的提高HashMap的效能。
同樣的問題也存在於LinkedHashMap中,因為LinkedHashMap本來就是一個HashMap,只是它還將所有Entry節點鏈入到了一個雙向連結串列中。LinkedHashMap完全繼承了HashMap的resize()方法,只是對它所呼叫的transfer方法進行了重寫。我們先看resize()方法原始碼:
/**
* Rehashes the contents of this map into a new array with a
* larger capacity. This method is called automatically when the
* number of keys in this map reaches its threshold.
*
* If current capacity is MAXIMUM_CAPACITY, this method does not
* resize the map, but sets threshold to Integer.MAX_VALUE.
* This has the effect of preventing future calls.
*
* @param newCapacity the new capacity, MUST be a power of two;
* must be greater than current capacity unless current
* capacity is MAXIMUM_CAPACITY (in which case value
* is irrelevant).
*/
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 若 oldCapacity 已達到最大值,直接將 threshold 設為 Integer.MAX_VALUE
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return; // 直接返回
}
// 否則,建立一個更大的陣列
Entry[] newTable = new Entry[newCapacity];
//將每條Entry重新雜湊到新的陣列中
transfer(newTable); //LinkedHashMap對它所呼叫的transfer方法進行了重寫
table = newTable;
threshold = (int)(newCapacity * loadFactor); // 重新設定 threshold
}
從上面程式碼中我們可以看出,Map擴容操作的核心在於重雜湊。所謂重雜湊是指重新計算原HashMap中的元素在新table陣列中的位置並進行復制處理的過程。鑑於效能和LinkedHashMap自身特點的考量,LinkedHashMap對重雜湊過程(transfer方法)進行了重寫,原始碼如下:
/**
* Transfers all entries to new table array. This method is called
* by superclass resize. It is overridden for performance, as it is
* faster to iterate using our linked list.
*/
void transfer(HashMap.Entry[] newTable) {
int newCapacity = newTable.length;
// 與HashMap相比,藉助於雙向連結串列的特點進行重雜湊使得程式碼更加簡潔
for (Entry<K,V> e = header.after; e != header; e = e.after) {
int index = indexFor(e.hash, newCapacity); // 計算每個Entry所在的桶
// 將其鏈入桶中的連結串列
e.next = newTable[index];
newTable[index] = e;
}
}
如上述原始碼所示,LinkedHashMap藉助於自身維護的雙向連結串列輕鬆地實現了重雜湊操作。若讀者想要進一步瞭解HashMap的重雜湊過程,請移步我的博文《Map 綜述(一):徹頭徹尾理解 HashMap》進行深入瞭解,此不贅述。
3、LinkedHashMap 的讀取實現 :get(Object key)
相對於LinkedHashMap的儲存而言,讀取就顯得比較簡單了。LinkedHashMap中重寫了HashMap中的get方法,原始碼如下:
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*/
public V get(Object key) {
// 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null) // 若不存在這樣的Entry,直接返回
return null;
e.recordAccess(this);
return e.value;
}
/**
* Returns the entry associated with the specified key in the
* HashMap. Returns null if the HashMap contains no mapping
* for the key.
*
* HashMap 中的方法
*
*/
final Entry<K,V> getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
在LinkedHashMap的get方法中,通過HashMap中的getEntry方法獲取Entry物件。注意這裡的recordAccess方法,如果連結串列中元素的排序規則是按照插入的先後順序排序的話,該方法什麼也不做;如果連結串列中元素的排序規則是按照訪問的先後順序排序的話,則將e移到連結串列的末尾處,筆者會在後文專門闡述這個問題。
另外,同樣地,呼叫LinkedHashMap的get(Object key)方法後,若返回值是 NULL,則也存在如下兩種可能:
- 該 key 對應的值就是 null;
- HashMap 中不存在該 key。
4、LinkedHashMap 存取小結
LinkedHashMap 的存取過程基本與HashMap基本類似,只是在細節實現上稍有不同,這是由LinkedHashMap本身的特性所決定的,因為它要額外維護一個雙向連結串列用於保持迭代順序。在put操作上,雖然LinkedHashMap完全繼承了HashMap的put操作,但是在細節上還是做了一定的調整,比如,在LinkedHashMap中向雜湊表中插入新Entry的同時,還會通過Entry的addBefore方法將其鏈入到雙向連結串列中。在擴容操作上,雖然LinkedHashMap完全繼承了HashMap的resize操作,但是鑑於效能和LinkedHashMap自身特點的考量,LinkedHashMap對其中的重雜湊過程(transfer方法)進行了重寫。在讀取操作上,LinkedHashMap中重寫了HashMap中的get方法,通過HashMap中的getEntry方法獲取Entry物件。在此基礎上,進一步獲取指定鍵對應的值。
六. LinkedHashMap 與 LRU(Least recently used,最近最少使用)演算法
到此為止,我們已經分析完了LinkedHashMap的存取實現,這與HashMap大體相同。LinkedHashMap區別於HashMap最大的一個不同點是,前者是有序的,而後者是無序的。為此,LinkedHashMap增加了兩個屬性用於保證順序,分別是雙向連結串列頭結點header和標誌位accessOrder。我們知道,header是LinkedHashMap所維護的雙向連結串列的頭結點,而accessOrder用於決定具體的迭代順序。實際上,accessOrder標誌位的作用可不像我們描述的這樣簡單,我們接下來仔細分析一波~
我們知道,當accessOrder標誌位為true時,表示雙向連結串列中的元素按照訪問的先後順序排列,可以看到,雖然Entry插入連結串列的順序依然是按照其put到LinkedHashMap中的順序,但put和get方法均有呼叫recordAccess方法(put方法在key相同時會呼叫)。recordAccess方法判斷accessOrder是否為true,如果是,則將當前訪問的Entry(put進來的Entry或get出來的Entry)移到雙向連結串列的尾部(key不相同時,put新Entry時,會呼叫addEntry,它會呼叫createEntry,該方法同樣將新插入的元素放入到雙向連結串列的尾部,既符合插入的先後順序,又符合訪問的先後順序,因為這時該Entry也被訪問了);當標誌位accessOrder的值為false時,表示雙向連結串列中的元素按照Entry插入LinkedHashMap到中的先後順序排序,即每次put到LinkedHashMap中的Entry都放在雙向連結串列的尾部,這樣遍歷雙向連結串列時,Entry的輸出順序便和插入的順序一致,這也是預設的雙向連結串列的儲存順序。因此,當標誌位accessOrder的值為false時,雖然也會呼叫recordAccess方法,但不做任何操作。
注意到我們在前面介紹的LinkedHashMap的五種構造方法,前四個構造方法都將accessOrder設為false,說明預設是按照插入順序排序的;而第五個構造方法可以自定義傳入的accessOrder的值,因此可以指定雙向迴圈連結串列中元素的排序規則。特別地,當我們要用LinkedHashMap實現LRU演算法時,就需要呼叫該構造方法並將accessOrder置為true。
1、put操作與標誌位accessOrder
/ 將key/value新增到LinkedHashMap中
public V put(K key, V value) {
// 若key為null,則將該鍵值對新增到table[0]中。
if (key == null)
return putForNullKey(value);
// 若key不為null,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若key對已經存在,則用新的value取代舊的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若key不存在,則將key/value鍵值對新增到table中
modCount++;
//將key/value鍵值對新增到table[i]處
addEntry(hash, key, value, i);
return null;
}
從上述原始碼我們可以看到,當要put進來的Entry的key在雜湊表中已經在存在時,會呼叫Entry的recordAccess方法;當該key不存在時,則會呼叫addEntry方法將新的Entry插入到對應桶的單鏈表的頭部。我們先來看recordAccess方法:
/**
* This method is invoked by the superclass whenever the value
* of a pre-existing entry is read by Map.get or modified by Map.set.
* If the enclosing Map is access-ordered, it moves the entry
* to the end of the list; otherwise, it does nothing.
*/
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
//如果連結串列中元素按照訪問順序排序,則將當前訪問的Entry移到雙向迴圈連結串列的尾部,
//如果是按照插入的先後順序排序,則不做任何事情。
if (lm.accessOrder) {
lm.modCount++;
//移除當前訪問的Entry
remove();
//將當前訪問的Entry插入到連結串列的尾部
addBefore(lm.header);
}
}
LinkedHashMap重寫了HashMap中的recordAccess方法(HashMap中該方法為空),當呼叫父類的put方法時,在發現key已經存在時,會呼叫該方法;當呼叫自己的get方法時,也會呼叫到該方法。該方法提供了LRU演算法的實現,它將最近使用的Entry放到雙向迴圈連結串列的尾部。也就是說,當accessOrder為true時,get方法和put方法都會呼叫recordAccess方法使得最近使用的Entry移到雙向連結串列的末尾;當accessOrder為預設值false時,從原始碼中可以看出recordAccess方法什麼也不會做。我們反過頭來,再看一下addEntry方法:
/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*
* LinkedHashMap中的addEntry方法
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
//建立新的Entry,並插入到LinkedHashMap中
createEntry(hash, key, value, bucketIndex); // 重寫了HashMap中的createEntry方法
//雙向連結串列的第一個有效節點(header後的那個節點)為最近最少使用的節點,這是用來支援LRU演算法的
Entry<K,V> eldest = header.after;
//如果有必要,則刪除掉該近期最少使用的節點,
//這要看對removeEldestEntry的覆寫,由於預設為false,因此預設是不做任何處理的。
if (removeEldestEntry(eldest)) {
removeEntryForKey(eldest.key);
} else {
//擴容到原來的2倍
if (size >= threshold)
resize(2 * table.length);
}
}
void createEntry(int hash, K key, V value, int bucketIndex) {
// 向雜湊表中插入Entry,這點與HashMap中相同
//建立新的Entry並將其鏈入到陣列對應桶的連結串列的頭結點處,
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
table[bucketIndex] = e;
//在每次向雜湊表插入Entry的同時,都會將其插入到雙向連結串列的尾部,
//這樣就按照Entry插入LinkedHashMap的先後順序來迭代元素(LinkedHashMap根據雙向連結串列重寫了迭代器)
//同時,新put進來的Entry是最近訪問的Entry,把其放在連結串列末尾 ,也符合LRU演算法的實現
e.addBefore(header);
size++;
}
同樣是將新的Entry鏈入到table中對應桶中的單鏈表中,但可以在createEntry方法中看出,同時也會把新put進來的Entry插入到了雙向連結串列的尾部。從插入順序的層面來說,新的Entry插入到雙向連結串列的尾部可以實現按照插入的先後順序來迭代Entry,而從訪問順序的層面來說,新put進來的Entry又是最近訪問的Entry,也應該將其放在雙向連結串列的尾部。在上面的addEntry方法中還呼叫了removeEldestEntry方法,該方法原始碼如下:
/**
* Returns <tt>true</tt> if this map should remove its eldest entry.
* This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
* inserting a new entry into the map. It provides the implementor
* with the opportunity to remove the eldest entry each time a new one
* is added. This is useful if the map represents a cache: it allows
* the map to reduce memory consumption by deleting stale entries.
*
* <p>Sample use: this override will allow the map to grow up to 100
* entries and then delete the eldest entry each time a new entry is
* added, maintaining a steady state of 100 entries.
* <pre>
* private static final int MAX_ENTRIES = 100;
*
* protected boolean removeEldestEntry(Map.Entry eldest) {
* return size() > MAX_ENTRIES;
* }
* </pre>
*
* <p>This method typically does not modify the map in any way,
* instead allowing the map to modify itself as directed by its
* return value. It <i>is</i> permitted for this method to modify
* the map directly, but if it does so, it <i>must</i> return
* <tt>false</tt> (indicating that the map should not attempt any
* further modification). The effects of returning <tt>true</tt>
* after modifying the map from within this method are unspecified.
*
* <p>This implementation merely returns <tt>false</tt> (so that this
* map acts like a normal map - the eldest element is never removed).
*
* @param eldest The least recently inserted entry in the map, or if
* this is an access-ordered map, the least recently accessed
* entry. This is the entry that will be removed it this
* method returns <tt>true</tt>. If the map was empty prior
* to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
* in this invocation, this will be the entry that was just
* inserted; in other words, if the map contains a single
* entry, the eldest entry is also the newest.
* @return <tt>true</tt> if the eldest entry should be removed
* from the map; <tt>false</tt> if it should be retained.
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
}
該方法是用來被重寫的,一般地,如果用LinkedHashmap實現LRU演算法,就要重寫該方法。比如可以將該方法覆寫為如果設定的記憶體已滿,則返回true,這樣當再次向LinkedHashMap中putEntry時,在呼叫的addEntry方法中便會將近期最少使用的節點刪除掉(header後的那個節點)。在第七節,筆者便重寫了該方法並實現了一個名副其實的LRU結構。
2、get操作與標誌位accessOrder
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*/
public V get(Object key) {
// 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null) // 若不存在這樣的Entry,直接返回
return null;
e.recordAccess(this);
return e.value;
}
在LinkedHashMap中進行讀取操作時,一樣也會呼叫recordAccess方法。上面筆者已經表述的很清楚了,此不贅述。
3、LinkedListMap與LRU小結
使用LinkedHashMap實現LRU的必要前提是將accessOrder標誌位設為true以便開啟按訪問順序排序的模式。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成為最近訪問的Entry,因此就把該Entry加入到了雙向連結串列的末尾:get方法通過呼叫recordAccess方法來實現;put方法在覆蓋已有key的情況下,也是通過呼叫recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現。這樣,我們便把最近使用的Entry放入到了雙向連結串列的後面。多次操作後,雙向連結串列前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除最前面的Entry(head後面的那個Entry)即可,因為它就是最近最少使用的Entry。
七. 使用LinkedHashMap實現LRU演算法
如下所示,筆者使用LinkedHashMap實現一個符合LRU演算法的資料結構,該結構最多可以快取6個元素,但元素多餘六個時,會自動刪除最近最久沒有被使用的元素,如下所示:
/**
* Title: 使用LinkedHashMap實現LRU演算法
* Description:
* @author rico
* @created 2017年5月12日 上午11:32:10
*/
public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{
private static final long serialVersionUID = 1L;
public LRU(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor, accessOrder);
}
/**
* @description 重寫LinkedHashMap中的removeEldestEntry方法,當LRU中元素多餘6個時,
* 刪除最不經常使用的元素
* @author rico
* @created 2017年5月12日 上午11:32:51
* @param eldest
* @return
* @see java.util.LinkedHashMap#removeEldestEntry(java.util.Map.Entry)
*/
@Override
protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
// TODO Auto-generated method stub
if(size() > 6){
return true;
}
return false;
}
public static void main(String[] args) {
LRU<Character, Integer> lru = new LRU<Character, Integer>(
16, 0.75f, true);
String s = "abcdefghijkl";
for (int i = 0; i < s.length(); i++) {
lru.put(s.charAt(i), i);
}
System.out.println("LRU中key為h的Entry的值為: " + lru.get('h'));
System.out.println("LRU的大小 :" + lru.size());
System.out.println("LRU :" + lru);
}
}
下圖是程式的執行結果:
八. LinkedHashMap 有序性原理分析
如前文所述,LinkedHashMap 增加了雙向連結串列頭結點header 和 標誌位accessOrder兩個屬性用於保證迭代順序。但是要想真正實現其有序性,還差臨門一腳,那就是重寫HashMap 的迭代器,其原始碼實現如下:
private abstract class LinkedHashIterator<T> implements Iterator<T> {
Entry<K,V> nextEntry = header.after;
Entry<K,V> lastReturned = null;
/**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount;
public boolean hasNext() { // 根據雙向列表判斷
return nextEntry != header;
}
public void remove() {
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;
}
}
// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator<K> {
public K next() { return nextEntry().getKey(); }
}
// Value 迭代器,Values(Collection)
private class ValueIterator extends LinkedHashIterator<V> {
public V next() { return nextEntry().value; }
}
// Entry 迭代器,EntrySet
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() { return nex