LinkedHashMap原始碼解析(jdk1.7之前)
1 LinkedHashMap(jdk1.7之前)
我們知道Map
其底層資料儲存是一個hash
表(陣列+單向連結串列)。接下來我們看一下另一個LinkedHashMap
,它是HashMap
的一個子類,他在HashMap
的基礎上維持了一個雙向連結串列(hash表
+雙向連結串列
),在遍歷的時候可以使用插入順序(先進先出,類似於FIFO
),或者是最近最少使用(LRU
)的順序。
來具體看下LinkedHashMap
的實現。
1.1 定義
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
從定義可以看到LinkedHashMap
繼承於HashMap
,且實現了Map
介面。這也就意味著HashMap
的一些優秀因素可以被繼承下來,比如hash
定址,使用連結串列解決hash
衝突等實現的快速查詢,對於HashMap
中一些效率較低的內容,比如容器擴容過程,遍歷方式,LinkedHashMap
是否做了一些優化呢。繼續看程式碼吧。
1.2 底層儲存
LinkedHashMap
是基於HashMap
LinkedHashMap
是一個hash
表(陣列+單向連結串列) +雙向連結串列的實現,到底實現方式是怎麼樣的,來看一下:
/** * 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. * * @serial11 */ private final boolean accessOrder;
看到了一個無比熟悉的屬性header
,它在LinkedList
中出現過,英文註釋很明確,是雙向連結串列的頭結點對不對。
再看accessOrder
這個屬性,true
表示最近較少使用順序,false
表示插入順序。當然你說怎麼沒看到陣列呢,別忘了LinkedHashMap
繼承於HashMap
再來看下Entry
這個節點類和HashMap
中的有什麼不同。
/** * LinkedHashMap entry.
*/
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
// 雙向連結串列的上一個節點before和下一個節點after
Entry<K,V> before, after ;
// 構造方法直接呼叫父類HashMap的構造方法(super)
Entry( int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
/** * 從連結串列中刪除當前節點的方法
*/
private void remove() {
// 改變當前節點前後兩個節點的引用關係,當前節點沒有被引用後,gc可以回收
// 將上一個節點的after指向下一個節點
before.after = after;
// 將下一個節點的before指向前一個節點
after.before = before;
}
/** * 在指定的節點前加入一個節點到連結串列中(也就是加入到連結串列尾部)
*/
private void addBefore(Entry<K,V> existingEntry) {
// 下面改變自己對前後的指向
// 將當前節點的after指向給定的節點(加入到existingEntry前面嘛)
after = existingEntry;
// 將當前節點的before指向給定節點的上一個節點33 before = existingEntry.before ;
// 下面改變前後最自己的指向
// 上一個節點的after指向自己
before.after = this;
// 下一個幾點的before指向自己
after.before = this;
}
// 當向Map中獲取查詢元素或修改元素(put相同key)的時候呼叫這個方法
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
// 如果accessOrder為true,也就是使用最近較少使用順序
if (lm.accessOrder ) {
lm. modCount++;
// 先刪除,再新增,也就相當於移動了
// 刪除當前元素
remove();
// 將當前元素加入到header前(也就是連結串列尾部)
addBefore(lm. header);
}
}
// 當從Map中刪除元素的時候調動這個方法
void recordRemoval(HashMap<K,V> m) {
remove();
}
}
可以看到Entry
繼承了HashMap
中的Entry
,但是LinkedHashMap
中的Entry
多了兩個屬性指向上一個節點的before
和指向下一個節點的after
,也正是這兩個屬性組成了一個雙向連結串列,Entry
還有一個繼承下來的next
屬性,這個next
是單向連結串列中用來指向下一個節點的,怎麼回事嘛,怎麼又是單向連結串列又是雙向連結串列呢,其實想的沒錯,這裡的節點即是Hash
表中的單向連結串列中的一個節點,它又是LinkedHashMap
維護的雙向連結串列中的一個節點,是不是瞬間覺得高大上了
注:黑色箭頭指向表示單向連結串列的next
指向,紅色箭頭指向表示雙向連結串列的before
指向,藍色箭頭指向表示雙向連結串列的after
指向。另外LinkedHashMap
種還有一個header
節點是不儲存資料的,這裡沒有畫出來。
從上圖可以看出LinkedHashMap
仍然是一個Hash
表,底層由一個數組組成,而陣列的每一項都是個單向連結串列,由next
指向下一個節點。但是LinkedHashMap
所不同的是,在節點中多了兩個屬性before
和after
,由這兩個屬性組成了一個雙向迴圈連結串列
,而由這個雙向連結串列維持著Map
容器中元素的順序。看下Entry
中的recordRemoval
方法,該方法將在節點被刪除時候呼叫,Hash
表中連結串列節點被正常刪除後,呼叫該方法修正由於節點被刪除後雙向連結串列的前後指向關係,從這一點來看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因為要維護雙向連結串列 )。
1.3 構造方法
/** * 構造一個指定初始容量和載入因子的LinkedHashMap,預設accessOrder為false
*/
public LinkedHashMap( int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/** * 構造一個指定初始容量的LinkedHashMap,預設accessOrder為false
*/
public LinkedHashMap( int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/** * 構造一個使用預設初始容量(16)和預設載入因子(0.75)的LinkedHashMap,預設accessOrder為false
*/
public LinkedHashMap() {
super();
accessOrder = false;
}
/** * 構造一個指定map的LinkedHashMap,所建立LinkedHashMap使用預設載入因子(0.75)和足以容納指定map的初始容量,預設accessOrder為false 。
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super(m);
accessOrder = false;
}
/** * 構造一個指定初始容量、載入因子和accessOrder的LinkedHashMap
*/
public LinkedHashMap( int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
40 this.accessOrder = accessOrder;
41 }
構造方法很簡單基本都是呼叫父類HashMap
的構造方法(super
),只有一個區別就是對於accessOrder
的設定,上面的構造引數中多數都是將accessOrder
預設設定為false
,只有一個構造方法留了一個出口可以設定accessOrder
引數。看完了構造方法,發現一個問題,頭部節點header
的初始化跑哪裡去了
回憶一下,看看HashMap的構造方法:
/** * Constructs an empty <tt>HashMap</tt> 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 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);
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[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() {
init()
在HashMap
中是一個空方法,也就是給子類留的一個回撥函式,我們來看下LinkedHashMap
對init()
方法的實現
/** * Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
void init() {
// 初始化話header,將hash設定為-1,key、value、next設定為null
header = new Entry<K,V>(-1, null, null, null);
// header的before和after都指向header自身
header.before = header. after = header ;
1.4 增加
LinkedHashMap
沒有重寫put
方法,只是重寫了HashMap
中被put
方法呼叫的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.
*/
void addEntry( int hash, K key, V value, int bucketIndex) {
// 呼叫createEntry方法建立一個新的節點
createEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed, else grow capacity if appropriate
// 取出header後的第一個節點(因為header不儲存資料,所以取header後的第一個節點)
Entry<K,V> eldest = header.after ;
// 判斷是容量不夠了是要刪除第一個節點還是需要擴容
if (removeEldestEntry(eldest)) {
// 刪除第一個節點(可實現FIFO、LRU策略的Cache)
removeEntryForKey(eldest. key);
} else {
// 和HashMap一樣進行擴容
if (size >= threshold)
resize(2 * table.length );
}
}
/** * This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
void createEntry( int hash, K key, V value, int bucketIndex) {
// 下面三行程式碼的邏輯是,建立一個新節點放到單向連結串列的頭部
// 取出陣列bucketIndex位置的舊節點
HashMap.Entry<K,V> old = table[bucketIndex];
// 建立一個新的節點,並將next指向舊節點
Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
// 將新建立的節點放到陣列的bucketIndex位置
table[bucketIndex] = e;
// 維護雙向連結串列,將新節點新增在雙向連結串列header前面(連結串列尾部)
e.addBefore( header);
// 計數器size加1
size++;
}
/** * 預設返回false,也就是不會進行元素刪除了。如果想實現cache功能,只需重寫該方法
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
可以看到,在新增方法上,比HashMap
中多了兩個邏輯,一個是當Map
容量不足後判斷是刪除第一個元素,還是進行擴容,另一個是維護雙向連結串列。而在判斷是否刪除元素的時候,我們發現removeEldestEntry
這個方法竟然是永遠返回false
,原來想要實現Cache
功能,需要自己繼承LinkedHashMap
然後重寫removeEldestEntry
方法,這裡預設提供的是容器的功能。
1.5 刪除
LinkedHashMap
沒有重寫remove
方法,只是在實現了Entry
類的recordRemoval
方法,該方法是HashMap
的提供的一個回撥方法,在HashMap
的remove
方法進行回撥,而LinkedHashMap
中recordRemoval
的主要當然是要維護雙向連結串列了
1.6 查詢
LinkedHashMap
重寫了get
方法,但是確複用了HashMap
中的getEntry
方法,LinkedHashMap
是在get
方法中指加入了呼叫recoreAccess
方法的邏輯,recoreAccess
方法的目的當然也是維護雙向連結串列了,具體邏輯返回上面去看下Entry
類的recoreAccess
方法吧
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess( this);
return e.value ;
}
1.7 是否包含
/** * Returns <tt>true</tt> if this map maps one or more keys to the
* specified value.
*
* @param value value whose presence in this map is to be tested
* @return <tt> true</tt> if this map maps one or more keys to the
* specified value
*/ public boolean containsValue(Object value) {
// Overridden to take advantage of faster iterator
// 遍歷雙向連結串列,查詢指定的value
if (value==null) {
for (Entry e = header .after; e != header; e = e.after )
if (e.value ==null)
return true;
} else {
for (Entry e = header .after; e != header; e = e.after )
if (value.equals(e.value ))
return true;
}
return false;
}
LinkedHashMap
對containsValue
進行了重寫,HashMap
的containsValue
需要遍歷整個hash
表,這樣是十分低效的。而LinkedHashMap
中重寫後,不再遍歷hash
表,而是遍歷其維護的雙向連結串列,這樣在效率上難道就有所改善嗎?我們分析下:hash
表是由陣列+單向連結串列組成,而由於使用hash
演算法,可能會導致雜湊不均勻,甚至陣列的有些項是沒有元素的(沒有hash
出對應的雜湊值),而LinkedHashMap
的雙向連結串列呢,是不存在空項的,所以LinkedHashMap
的containsValue
比HashMap
的containsValue
效率要好一些。
1.8 cache功能
在最後,讓我們簡單基於LInkedHashMap
實現一個Cache功能
import java.util.LinkedHashMap;
import java.util.Map;
public class MyLocalCache extends LinkedHashMap<String, Object> {
private static final long serialVersionUID = 7182816356402068265L;
private static final int DEFAULT_MAX_CAPACITY = 1024;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private int maxCapacity;
public enum Policy {
FIFO, LRU
}
public MyLocalCache(Policy policy) {
super(DEFAULT_MAX_CAPACITY, DEFAULT_LOAD_FACTOR, Policy.LRU .equals(policy));
this.maxCapacity = DEFAULT_MAX_CAPACITY;
}
public MyLocalCache(int capacity, Policy policy) {
super(capacity, DEFAULT_LOAD_FACTOR, Policy. LRU.equals(policy));
this.maxCapacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return this.size() > maxCapacity;
}
public static void main(String[] args) {
MyLocalCache cache = new MyLocalCache(5, Policy.LRU);
cache.put( "k1", "v1" );
cache.put( "k2", "v2" );
cache.put( "k3", "v3" );
cache.put( "k4", "v4" );
cache.put( "k5", "v5" );
cache.put( "k6", "v6" );
System. out.println("size=" + cache.size());
System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
System. out.println(entry.getValue());
}
System. out.println("----------------------" );
System. out.println("k3=" + cache.get("k3"));
System. out.println("----------------------" );
for (Map.Entry<String, Object> entry : cache.entrySet()) {
System. out.println(entry.getValue());
}
}
}