Java集合系列之LinkedHashMap
Hello,大家好,前面給大家講了HashMap,LinkedList,知道了HashMap為陣列+單向連結串列,LinkedList為雙向連結串列實現的。今天給大家介紹一個(HashMap+"LinkedList")的集合,LinkedHashMap,其中HashMap用於儲存資料,"LinkedList"用於儲存資料順序。OK,廢話少說,老套路,文章結構:
- LinkedHashMap和HashMap區別
- LinkedHashMap底層實現
- 利用LinkedHashMap實現LRU快取
1. LinkedHashMap和HashMap區別
大多數情況下,只要不涉及執行緒安全問題,Map基本都可以使用HashMap,不過HashMap有一個問題,就是迭代HashMap的順序並不是HashMap放置的順序,也就是無序。HashMap的這一缺點往往會帶來困擾,因為有些場景,我們期待一個有序的Map.這就是我們的LinkedHashMap,看個小Demo:
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("apple", "蘋果");
map.put("watermelon", "西瓜");
map.put("banana", "香蕉");
map.put("peach", "桃子");
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
複製程式碼
輸出為:
apple=蘋果
watermelon=西瓜
banana=香蕉
peach=桃子
複製程式碼
可以看到,在使用上,LinkedHashMap和HashMap的區別就是LinkedHashMap是有序的。 上面這個例子是根據插入順序排序,此外,LinkedHashMap還有一個引數決定是否在此基礎上再根據訪問順序(get,put)排序,記住,是在插入順序的基礎上再排序,後面看了原始碼就知道為什麼了。看下例子:
public static void main(String[] args) {
Map<String, String> map = new LinkedHashMap<String, String>(16,0.75f,true);
map.put("apple", "蘋果");
map.put("watermelon", "西瓜");
map.put("banana", "香蕉");
map.put("peach", "桃子");
map.get("banana");
map.get("apple");
Iterator iter = map.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
System.out.println(entry.getKey() + "=" + entry.getValue());
}
}
複製程式碼
輸出為:
watermelon=西瓜
peach=桃子
banana=香蕉
apple=蘋果
複製程式碼
可以看到香蕉和蘋果在原來排序的基礎上又排後了。
2. LinkedHashMap底層實現
我先說結論,然後再慢慢跟程式碼。
- LinkedHashMap繼承自HashMap,它的新增(put)和獲取(get)方法都是複用父類的HashMap的程式碼,只是自己重寫了put給get內部的某些介面來搞事情,這個特性在C++中叫鉤子技術,在Java裡面大家喜歡叫多型,其實多型這個詞並不能很好的形容這種現象。
- LinkedHashMap的資料儲存和HashMap的結構一樣採用(陣列+單向連結串列)的形式,只是在每次節點Entry中增加了用於維護順序的before和after變數維護了一個雙向連結串列來儲存LinkedHashMap的儲存順序,當呼叫迭代器的時候不再使用HashMap的的迭代器,而是自己寫迭代器來遍歷這個雙向連結串列即可。
- HashMap和LinkedHashMap內部邏輯圖如下:
好了,大家肯定會覺得很神奇,如圖所示,本來HashMap的資料是0-7這樣的無須的,而LinkedHashMap卻把它變成了如圖所示的1.6.5.3.。。2這樣的有順序了。到底是如何做到的了?其實說白了,就一句話,鉤子技術,在put和get的時候維護好了這個雙向連結串列,遍歷的時候就有序了。好了,一步一步的跟。 先看一下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);
}
...
}
複製程式碼
可以看到繼承自HashMap的Entry,並且多了兩個指標before和after,這兩個指標說白了,就是為了維護雙向連結串列新加的兩個指標。 列一下新Entry的所有成員變數吧:
- K key
- V value
- Entry<K, V> next
- int hash
- Entry<K, V> before
- Entry<K, V> after
其中前面四個,是從HashMap.Entry中繼承過來的;後面兩個,是是LinkedHashMap獨有的。不要搞錯了next和before、After,next是用於維護HashMap指定table位置上連線的Entry的順序的,before、After是用於維護Entry插入的先後順序的(為了維護雙向連結串列)。
2.1 初始化
1 public LinkedHashMap() {
2 super();
3 accessOrder = false;
4 }
複製程式碼
1 public HashMap() {
2 this.loadFactor = DEFAULT_LOAD_FACTOR;
3 threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
4 table = new Entry[DEFAULT_INITIAL_CAPACITY];
5 init();
6 }
複製程式碼
1 void init() {
2 header = new Entry<K,V>(-1, null, null, null);
3 header.before = header.after = header;
4 }
複製程式碼
這裡出現了第一個鉤子技術,儘管init()方法定義在HashMap中,但是由於LinkedHashMap重寫了init方法,所以根據多型的語法,會呼叫LinkedHashMap的init方法,該方法初始化了一個Header,這個Header就是雙向連結串列的連結串列頭..
2.2 LinkedHashMap新增元素
HashMap中的put方法:
1 public V put(K key, V value) {
2 if (key == null)
3 return putForNullKey(value);
4 int hash = hash(key.hashCode());
5 int i = indexFor(hash, table.length);
6 for (Entry<K,V> e = table[i]; e != null; e = e.next) {
7 Object k;
8 if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
9 V oldValue = e.value;
10 e.value = value;
11 e.recordAccess(this);
12 return oldValue;
13 }
14 }
15
16 modCount++;
17 addEntry(hash, key, value, i);
18 return null;
19 }
複製程式碼
LinkedHashMap中的addEntry(又是一個鉤子技術):
1 void addEntry(int hash, K key, V value, int bucketIndex) {
2 createEntry(hash, key, value, bucketIndex);
3
4 // Remove eldest entry if instructed, else grow capacity if appropriate
5 Entry<K,V> eldest = header.after;
6 if (removeEldestEntry(eldest)) {
7 removeEntryForKey(eldest.key);
8 } else {
9 if (size >= threshold)
10 resize(2 * table.length);
11 }
12 }
複製程式碼
1 void createEntry(int hash, K key, V value, int bucketIndex) {
2 HashMap.Entry<K,V> old = table[bucketIndex];
3 Entry<K,V> e = new Entry<K,V>(hash, key, value, old);
4 table[bucketIndex] = e;
5 e.addBefore(header);
6 size++;
7 }
複製程式碼
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
複製程式碼
好了,addEntry先把資料加到HashMap中的結構中(陣列+單向連結串列),然後呼叫addBefore,這個我就不和大家畫圖了,其實就是挪動自己和Header的Before與After成員變數指標把自己加到雙向連結串列的尾巴上。 同樣的,無論put多少次,都會把當前元素加到佇列尾巴上。這下大家知道怎麼維護這個雙向佇列的了吧。
上面說了LinkedHashMap在新增資料的時候自動維護了雙向列表,這要還要提一下的是LinkedHashMap的另外一個屬性,根據查詢順序排序,說白了,就是在get的時候或者put(更新時)把元素丟到雙向佇列的尾巴上。這樣不就排序了嗎?這裡涉及到LinkedHashMap的另外一個構造方法:
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
複製程式碼
第三個引數,accessOrder為是否開啟查詢排序功能的開關,預設為False。如果想開啟那麼必須呼叫這個構造方法。 然後看下get和put(更新操作)時是如何維護這個佇列的。
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;
}
複製程式碼
此外,在put的時候,程式碼11行(見上面的程式碼),也是呼叫了e.recordAccess(this);我們來看下這個方法:
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove();
addBefore(lm.header);
}
}
複製程式碼
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;
}
複製程式碼
看到每次recordAccess的時候做了兩件事情:
- 把待移動的Entry的前後Entry相連
- 把待移動的Entry移動到尾部
當然,這一切都是基於accessOrder=true的情況下。 假設現在我們開啟了accessOrder,然後呼叫get("111");看下是如何操作的:
3. 利用LinkedHashMap實現LRU快取
LRU即Least Recently Used,最近最少使用,也就是說,當快取滿了,會優先淘汰那些最近最不常訪問的資料。我們的LinkedHashMap正好滿足這個特性,為什麼呢?當我們開啟accessOrder為true時,最新訪問(get或者put(更新操作))的資料會被丟到佇列的尾巴處,那麼雙向佇列的頭就是最不經常使用的資料了。比如:
如果有1 2 3這3個Entry,那麼訪問了1,就把1移到尾部去,即2 3 1。每次訪問都把訪問的那個資料移到雙向佇列的尾部去,那麼每次要淘汰資料的時候,雙向佇列最頭的那個資料不就是最不常訪問的那個資料了嗎?換句話說,雙向連結串列最頭的那個資料就是要淘汰的資料。
此外,LinkedHashMap還提供了一個方法,這個方法就是為了我們實現LRU快取而提供的,removeEldestEntry(Map.Entry<K,V> eldest) 方法。該方法可以提供在每次新增新條目時移除最舊條目的實現程式,預設返回 false。
來,給大家一個簡陋的LRU快取:
public class LRUCache extends LinkedHashMap
{
public LRUCache(int maxSize)
{
super(maxSize, 0.75F, true);
maxElements = maxSize;
}
protected boolean removeEldestEntry(java.util.Map.Entry eldest)
{
//邏輯很簡單,當大小超出了Map的容量,就移除掉雙向佇列頭部的元素,給其他元素騰出點地來。
return size() > maxElements;
}
private static final long serialVersionUID = 1L;
protected int maxElements;
}
複製程式碼
是不是很簡單。。
結語
其實 LinkedHashMap 幾乎和 HashMap 一樣:從技術上來說,不同的是它定義了一個 Entry<K,V> header,這個 header 不是放在 Table 裡,它是額外獨立出來的。LinkedHashMap 通過繼承 hashMap 中的 Entry<K,V>,並新增兩個屬性 Entry<K,V> before,after,和 header 結合起來組成一個雙向連結串列,來實現按插入順序或訪問順序排序。如何維護這個雙向連結串列了,就是在get和put的時候用了鉤子技術(多型)呼叫LinkedHashMap重寫的方法來維護這個雙向連結串列,然後迭代的時候直接迭代這個雙向連結串列即可,好了LinkedHashMap算是給大家分享完了,Over,Have a good day .