Java集合系列之LinkedHashMap

Java集合系列之LinkedHashMap

Hello,你們好,前面給你們講了HashMap,LinkedList,知道了HashMap爲數組+單向鏈表,LinkedList爲雙向鏈表實現的。今天給你們介紹一個(HashMap+"LinkedList")的集合,LinkedHashMap,其中HashMap用於存儲數據,"LinkedList"用於存儲數據順序。OK,廢話少說,老套路,文章結構:java

  1. LinkedHashMap和HashMap區別
  2. LinkedHashMap底層實現
  3. 利用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底層實現

我先說結論,而後再慢慢跟代碼。bash

  • 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(也就是每一個元素):app

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的全部成員變量吧:this

  • 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插入的前後順序的(爲了維護雙向鏈表)。spa

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的時候作了兩件事情:

  1. 把待移動的Entry的先後Entry相連
  2. 把待移動的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 .

相關文章
相關標籤/搜索