【源碼】LinkedHashMap源碼剖析,linkedhashmap源碼
註:以下源碼基於jdk1.7.0_11
之前的兩篇文章通過源碼分析了兩種常見的Map集合,HashMap和Hashtable。本文將繼續介紹另一種Map集合——LinkedHashMap。顧名思義,LinkedHashMap除了是一個HashMap之外,還帶有LinkedList的特點,也就是說能夠保持遍曆的順序和插入的順序一致,那麼它是怎麼做到的呢?下面我們開始分析。首先看構造器。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap直接繼承自HashMap,所以擁有HashMap的大部分特性,比如支援null鍵和值,預設容量為16,裝載因子為0.75,非安全執行緒等等。但是LinkedHashMap還有很多個性的地方,下面來看成員變數:
private transient Entry<K,V> header;//內部雙向鏈表的頭結點 /** *代表這個鏈表的排序方式,true代表按照訪問順序,false代表按照插入順序。 */ private final boolean accessOrder;
LinkedHashMap比HashMap多了兩個成員變數,其中header代表內部雙向鏈表的頭結點,後面我們就會發現,LinkedHashMap除了有個桶數組容納所有Entry之外,還有一個雙向鏈表儲存所有Entry引用。遍
曆的時候,並不是去遍曆桶數組,而是直接遍曆雙向鏈表,所以LinkedHashMap的遍曆時間不受桶容量的限制,這是它和HashMap的重要區別之一。而這個accessOrder代表的是是否按照訪問順序,true代表是,預設是插入順序。所以我們可以將accessOrder置為true來實現LRU演算法,這可以用來做緩衝。再看構造器:
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(m); accessOrder = false; } public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
構造器首先都會調用父類也就是HashMap的構造器來初始化桶數組,而accessOrder之後會被初始化,除了最後面的一個構造器允許指定accessOrder外,其他構造器都預設將accessOrder置為了false。讀者可能很奇怪,不是還有個header麼,這個雙向鏈表為啥不在構造器中初始化呢?這得回到HashMap中查看hashMap的構造器了:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); ... ... init(); }
HashMap構造器最後一步調用了一個init方法,而這個init方法在HashMap中是個空實現,沒有任何代碼。
這其實就是所謂的“鉤子”,具體代碼由子類實現,如果子類希望每次構造的時候都去做一些特定的初始化操作,可以選擇複寫init方法。我們看到LinkedHashMap中確實複寫了init:
@Override void init() { header = new Entry<>(-1, null, null, null);//初始化雙向鏈表 header.before = header.after = header;//不光是雙向鏈表,還是迴圈鏈表 }
在init方法中,果然初始化了雙向鏈表,而且我們還發現,這不光是個雙向鏈表,還是個迴圈鏈表。
HashMap內部的Entry類並沒有before和after指標,
也就是說LinkedHashMap自己重寫了一個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); } /** * Removes this entry from the linked list. */ private void remove() { before.after = after; after.before = before; } /** * Inserts this entry before the specified existing entry in the list. */ private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } /** * 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; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } void recordRemoval(HashMap<K,V> m) { remove(); } }這裡的Entry選擇繼承父類的Entry類,也就是說
LinkedHashMap中的Entry擁有三個指標,除了前驅後繼指標外用於雙向鏈表的串連外,還有一個next指標用於解決hash衝突(引用鏈)。除此之外,Entry新增了幾個方法,remove和addbefore用來操作雙向鏈表不用多說。而recordAccess方法比較特殊,這個方法在HashMap中也是空實現,並在put方法中會調用此方法:
public V put(K key, V value) {//HashMap的put方法 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; e.recordAccess(this);//發生覆蓋操作時,會調用此方法 return oldValue; } } ... ... }
此外,在LinkedHashMap的get方法中,也會調用此方法:
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; }
也就是說,只要涉及到訪問結點,那麼就會調用這個方法。
觀察該方法的邏輯:如果accessOrder為true,那麼會調用addBefore方法將當前Entry放到雙向鏈表的尾部,最終在我們遍曆鏈表的時候就會發現最近最少使用的結點的都集中在鏈表頭部(從近期訪問最少到近期訪問最多的順序),這就是LRU。
LinkedHashMap並沒有複寫put方法,但是卻複寫了addEntry和createEntry方法,之前分析HashMap的時候我們就知道了,put方法會調用addEntry將鍵值對掛到桶的某個合適位置,而addEntry又會調用createEntry方法建立一個鍵值對對象。因而,LinkedHashMap其實是間接更改了put方法,想想也很容易理解,LinkedHashMap除了要向桶中添加鍵值對外,還需向鏈表中增加鍵值對,所以必須得修改put方法。
void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed Entry<K,V> eldest = header.after;//標記最少訪問的對象 if (removeEldestEntry(eldest)) {//判斷是否需要刪除這個對象---->可由子類實現來提供緩衝功能 removeEntryForKey(eldest.key); } } 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; e.addBefore(header);//添加到鏈表尾部 size++; }
createEntry方法會將鍵值對分別掛到桶數組和雙向鏈表中。比較有意思的是addEntry方法,它提供了一個可選的操作,我們可以通過繼承LinkedHashMap並複寫removeEldestEntry方法讓該子類可以自動地刪除最近最少訪問的鍵值對——這可以用來做緩衝!!
LinkedHashMap自訂了迭代器以及迭代規則,LinkedHashMap是通過內部的雙向鏈表來完成迭代的,遍曆時間與鍵值對總數成正比,而HashMap遍曆時間與容量成正比,所以通常情況下,LinkedHashMap遍曆效能是優於HashMap的,但是因為需要額外維護鏈表,所以折中來看,兩者效能相差無幾。
private abstract class LinkedHashIterator<T> implements Iterator<T> { Entry<K,V> nextEntry = header.after;//指向鏈表首部 Entry<K,V> lastReturned = null; 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; } }總結:1.LinkedHashMap繼承自HashMap,具有HashMap的大部分特性,比如支援null鍵和值,預設容量為16,裝載因子為0.75,非安全執行緒等等;2.LinkedHashMap通過設定accessOrder控制遍曆順序是按照插入順序還是按照訪問順序。當accessOrder為true時,可以利用其完成LRU緩衝的功能;3.LinkedHashMap內部維護了一個雙向迴圈鏈表,並且其迭代操作時通過鏈表完成的,而不是去遍曆hash表。
初學者可以看得懂《STL源碼剖析》?
《STL源碼剖析》不是講怎麼樣使用STL和STL技巧的,是關於STL核心代碼的剖析,是面向有豐富經驗的STL程式員來補充和更好的理解STL底層核心機制,初學者看這本書的話基本上是一頭霧水,建議先從基礎學起,C++標準程式庫 和C++stl是比較好的入門且使用的書籍,以後有了一定的STL經驗,再去研究STL源碼剖析,相信那時候你就會有了另一番對STL的領悟。
錯誤提示