Java集合框架分析(五)LinkedHashMap分析

LinkedHashMap簡介

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
複製代碼

繼承自 HashMap,一個有序的 Map 接口實現,這裏的有序指的是元素能夠按插入順序或訪問順序排列;與 HashMap 相比,由於 LinkedHashMap 是繼承自 HashMap,所以LinkedHashMap,一樣是基於散列表實現。同時實現了 Serializable 和 Cloneable 接口,支持序列化和克隆。而且一樣不是線程安全的。區別是其內部維護了一個雙向循環鏈表,該鏈表是有序的,能夠按元素插入順序或元素最近訪問順序 (LRU) 排列。算法

LinkedHashMap數據結構

LinkedHashMap 不只像 HashMap 那樣對其進行基於哈希表和單鏈表的 Entry 數組+ next 鏈表的存儲方式,並且還結合了 LinkedList 的優勢,爲每一個 Entry 節點增長了前驅和後繼,並增長了一個爲 header 頭結點,構造了一個雙向循環鏈表。也就是說,每次 put 進來 KV,除了將其保存到對哈希表中的對應位置外,還要將其插入到雙向循環鏈表的尾部。數組

在這裏插入圖片描述

上圖是 LinkedHashMap 的所有數據結構,包含散列表和循環雙向鏈表,因爲循環雙向鏈表線條太多了,很差畫,簡單的畫了一個節點(黃色圈出來的)示意一下,注意左邊的紅色箭頭引用爲 Entry 節點對象的 next 引用(散列表中的單鏈表),綠色線條爲 Entry 節點對象的 before, after 引用(循環雙向鏈表的先後引用);緩存

在這裏插入圖片描述

上圖專門把循環雙向鏈表抽取出來,直觀一點,注意該循環雙向鏈表的頭部存放的是最久訪問的節點或最早插入的節點,尾部爲最近訪問的或最近插入的節點,迭代器遍歷方向是從鏈表的頭部開始到鏈表尾部結束,在鏈表尾部有一個空的 header 節點,該節點不存放 key-value 內容,爲 LinkedHashMap 類的成員屬性,循環雙向鏈表的入口;安全

LinkedHashMap源碼

上面是分析 LinkedHashMap 源碼的常規知識點,瞭解一下,才能更好的分析它的源碼,下面咱們便開始正式的進行分析工做。bash

屬性:

//屬性設置,序列化ID
private static final long serialVersionUID = 3801124242820219131L;
//雙向鏈表的頭部
private transient LinkedHashMapEntry<K,V> header;
//迭代的時候所用到的順序,若是爲FALSE,則按照插入的時候順序
private final boolean accessOrder;
複製代碼

這些屬性雖然簡單,可是比較重要,一開始就直接詳細說明,不大好理解,等咱們分析完了代碼再來回顧一下它們所表示的意思。咱們來分析分析它的構造函數。數據結構

構造器分析

設置初始容量和加載因子的構造器app

/**
  * 設置初始容量和加載因子的構造器
  */
 public LinkedHashMap(int initialCapacity, float loadFactor) {
     super(initialCapacity, loadFactor);
     accessOrder = false;
 }
複製代碼

設置初始容量的構造器ide

/**
  * 設置初始容量的構造器
  * @param  initialCapacity the initial capacity
  * @throws IllegalArgumentException if the initial capacity is negative
  */
 public LinkedHashMap(int initialCapacity) {
     super(initialCapacity);
     accessOrder = false;
 }
複製代碼

默認的空參數的構造器,默認容量爲16以及加載因子爲0.75函數

/**
  * 默認的空參數的構造器,默認容量爲16以及加載因子爲0.75
  * with the default initial capacity (16) and load factor (0.75).
  */
 public LinkedHashMap() {
     super();
     accessOrder = false;
 }
複製代碼

使用一個現有的Map來構造LinkedHashMap學習

/**
  * 使用一個現有的Map來構造LinkedHashMap
  * @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);
     accessOrder = false;
 }
複製代碼

設定迭代順序的構造器

/**
  * 設定迭代順序的構造器
  * @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);
     this.accessOrder = accessOrder;
 }
複製代碼

這些構造器都比較簡單,咱們稍微說起一下,若未指定初始容量 initialCapacity,則默認爲使用 HashMap 的初始容量,即 16。若未指定加載因子 loadFactor,則默認爲 0.75。accessOrder 默認爲 faslse。這裏須要介紹一下這個布爾值,它是雙向鏈表中元素排序規則的標誌位。

accessOrder 若爲 false,遍歷雙向鏈表時,是按照插入順序排序。 accessOrder 若爲 true,表示雙向鏈表中的元素按照訪問的前後順序排列,最早遍歷到(鏈表頭)的是最近最少使用的元素。

從構造方法中能夠看出,默認都採用插入順序來維持取出鍵值對的次序。全部構造方法都是經過調用父類的構造方法來建立對象的。

在父類的構造器中咱們能夠看到它調用了 init 方法,即在 Map 類中的構造器中調用了 init 方法,咱們進入查看一下內容

@Override
    void init() {
        header = new LinkedHashMapEntry<>(-1, null, null, null);
        header.before = header.after = header;
    }
複製代碼

這個 init 方法主要是對 header 節點進行初始化的,構成一個雙向鏈表。分析完了構造器,接着咱們分析一下最多見的一個屬性 Entry。

LinkedHashMapEntry分析

//這個Entry繼承自HashMapEntry
 private static class LinkedHashMapEntry<K,V> extends HashMapEntry<K,V> {
        //雙向節點的先後引用
        // These fields comprise the doubly linked list used for iteration.
        LinkedHashMapEntry<K,V> before, after;
        
        //構造器
        LinkedHashMapEntry(int hash, K key, V value, HashMapEntry<K,V> next) {
            super(hash, key, value, next);
        }
        //移除一個節點
        private void remove() {
            before.after = after;
            after.before = before;
        }
        /**
         * 在指定的位置前面插入一個節點
         */
        private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
        /*
         *在HashMap的put和get方法中,會調用該方法,在HashMap中該方法爲空
         * 在LinkedHashMap中,當按訪問順序排序時,該方法會將當前節點插入到鏈表尾部(頭結點的前一個節點),不然不作任何事
         */
        void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //當LinkedHashMap按訪問排序時
            if (lm.accessOrder) {
                lm.modCount++;
                //移除當前節點
                remove();
                //將當前節點插入到頭結點前面
                addBefore(lm.header);
            }
        }
        void recordRemoval(HashMap<K,V> m) {
            remove();
        }
    }
複製代碼

接着分析最經常使用的方法 put。

put分析

咱們在使用 LinkedHashMap 的 put 方法時,發現它調用的是 HashMap 的 put 方法,本身自己沒有複寫 put 方法,因此這種狀況下,咱們就得分兩種狀況來討論 LinkedHashMap 的 put 操做了。

Key已存在的狀況

在 HashMap 的 put 方法中,在發現插入的 key 已經存在時,除了作替換工做,還會調用recordAccess() 方法,在 HashMap 中該方法爲空。LinkedHashMap 覆寫了該方法,(調用LinkedHashmap 覆寫的 get 方法時,也會調用到該方法),LinkedHashMap 並無覆寫 HashMap 中的 put 方法,recordAccess() 在 LinkedHashMap 中的實現以下:

//若是當前標明的accessOrder爲TRUE的話,則將當前訪問的Entry放置到雙向循環鏈表的尾部,以標明最近訪問 ,這也是爲何在HashMap.Entry中有一個空的 recordAccess(HashMap<K,V> m)方法的緣由
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            //LRU算法,將訪問的節點插入到鏈表尾部
            if (lm.accessOrder) {
                lm.modCount++;
                //刪除當前節點
                remove();
                //將當前節點插入到頭結點前面
                addBefore(lm.header);
            }
        }
        
//將當前節點插入到頭結點前面
private void addBefore(LinkedHashMapEntry<K,V> existingEntry) {
            after  = existingEntry;
            before = existingEntry.before;
            before.after = this;
            after.before = this;
        }
     
複製代碼

key不存在的狀況下

在 put 新 Entry 的過程當中,若是發現 key 不存在時,除了將新 Entry 放到哈希表的相應位置,還會調用 addEntry 方法,它會調用 creatEntry 方法,該方法將新插入的元素放到雙向鏈表的尾部,這樣作既符合插入的前後順序,又符合了訪問的前後順序。

//建立節點,插入到LinkedHashMap中,該方法覆蓋HashMap的addEntry方法
void addEntry(int hash, K key, V value, int bucketIndex) {
        //注意頭結點的下個節點即header.after,存放於鏈表頭部,是最不常常訪問或第一個插入的節點,
        LinkedHashMapEntry<K,V> eldest = header.after;
        //若是有必要,則刪除掉該近期最少使用的節點
        if (eldest != header) {
            boolean removeEldest;
            size++;
            try {
                //removeEldestEntry方法的實現,這裏默認爲false
                removeEldest = removeEldestEntry(eldest);
            } finally {
                size--;
            }
            if (removeEldest) {
                removeEntryForKey(eldest.key);
            }
        }
        //調用HashMap的addEntry方法
        super.addEntry(hash, key, value, bucketIndex);
    }
    
//建立節點,並將該節點插入到鏈表尾部
 void createEntry(int hash, K key, V value, int bucketIndex) {
        HashMapEntry<K,V> old = table[bucketIndex];
        LinkedHashMapEntry<K,V> e = new LinkedHashMapEntry<>(hash, key, value, old);
        table[bucketIndex] = e;
        //並將其移到雙向鏈表的尾部  
        e.addBefore(header);
        size++;
    }
複製代碼

在上面的 addEntry 方法中有一個 removeEldestEntry 方法,這個方法能夠被覆寫,好比能夠將該方法覆寫爲若是設定的內存已滿,則返回 true,這樣就能夠將最近最少使用的節點(header 後的節點)刪除掉。

爲何這個方法始終返回 false?

結合上面的 addEntry(int hash,K key,V value,int bucketIndex) 方法,這樣設計可使LinkedHashMap 成爲一個正常的 Map,不會去移除「最老」的節點。 爲何不在代碼中直接去除這部分邏輯而是設計成這樣呢?這爲開發者提供了方便,若但願將 Map 當作 Cache 來使用,而且限制大小,只需繼承 LinkedHashMap 並重寫 removeEldestEntry(Entry<K,V> eldest) 方法,像這樣:

private static final int MAX_ENTRIES = 100;
protected boolean removeEldestEntry(Map.Entry eldest) {
      return size() > MAX_ENTRIES;
}
複製代碼

總結一下 只要是 put 進來的新元素,無論 accessOrder 標誌位是什麼,均將新元素放到雙鏈表尾部,而且能夠在須要實現Lru算法時時覆寫 removeEldestEntry 方法,剔除最近最少使用的節點。

get分析

//覆寫HashMap中的get方法,經過getEntry方法獲取Entry對象。  
//注意這裏的recordAccess方法,  
//若是鏈表中元素的排序規則是按照插入的前後順序排序的話,該方法什麼也不作,  
//若是鏈表中元素的排序規則是按照訪問的前後順序排序的話,則將e移到鏈表的末尾處。
public V get(Object key) {
        LinkedHashMapEntry<K,V> e = (LinkedHashMapEntry<K,V>)getEntry(key);
        if (e == null)
            return null;
        e.recordAccess(this);
        return e.value;
    }
    
void recordAccess(HashMap<K,V> m) {
            LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
            if (lm.accessOrder) {
                lm.modCount++;
                remove();
                addBefore(lm.header);
            }
        }
複製代碼

get(Object key) 方法經過 HashMap 的 getEntry(Object key) 方法獲取節點,並返回該節點的 value 值,獲取節點若是爲 null 則返回 null。經過 key 獲取 value,與 HashMap 的區別是:當 LinkedHashMap 按訪問順序排序的時候,會將訪問的當前節點移到鏈表尾部(頭結點的前一個節點)。

到這裏咱們來具體總結一下 accessOrder 標誌位的做用原理。

一、accessOrder 不起做用

對於 put 操做時,無論 accessOrder 標誌位是什麼,咱們都將節點插入到鏈表的尾部,可是呢,能夠在須要實現 Lru 算法時時覆寫 removeEldestEntry 方法,剔除最近最少使用的節點。

二、accessOrder 起做用

當咱們進行 put 操做是,若是 key 不等於 null 的話,會調用 recordAccess 方法,在該方法中 accessOrder 就得起做用了,若是 accessOrder 爲 fasle 時,什麼也不作,也就是說當咱們放入已經存在 Key 的鍵值對,它在雙鏈表中的位置是不會變的。accessOrder 設置爲 true 時, put 操做會將相關元素放置到雙鏈表的尾部。

另一種狀況就是 get 操做,get 操做咱們同時也會調用 recordAccess 方法,對於這個方法,咱們須要判斷 accessOrder 的狀態,若是 accessOrder 爲 fasle 時,什麼也不作,也就是說當咱們放入已經存在 Key 的鍵值對,它在雙鏈表中的位置是不會變的。accessOrder 設置爲 true 時,put 操做會將相關元素放置到雙鏈表的尾部。在緩存的角度來看,這就是所謂的「髒數據」,即最近被訪問過的數據,所以在須要清理內存時(添加進新元素時),就能夠將雙鏈表頭節點(空節點)後面那個節點剔除。

不經常使用方法

到此爲止,基本上 LinkedHashMap 比較重要的方法就分析過了,還剩一些比較不重要的方法,咱們一次性給它注視下,稍微看下。

//
@Override
void transfer(HashMapEntry[] newTable) {
    int newCapacity = newTable.length;
    for (LinkedHashMapEntry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);
        e.next = newTable[index];
        newTable[index] = e;
    }
}
複製代碼

transfer(HashMap.Entry[] newTable) 方法和 init() 方法同樣也在 HashTable 中被調用。transfer(HashMap.Entry[] newTable) 方法在 HashMap 調用 resize(int newCapacity) 方法的時候被調用。根據鏈表節點 e 的哈希值計算 e 在新容量的 table 數組中的索引,並將 e 插入到計算出的索引所引用的鏈表中。

public boolean containsValue(Object value) {
        // Overridden to take advantage of faster iterator
        if (value==null) {
            for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
                if (e.value==null)
                    return true;
        } else {
            for (LinkedHashMapEntry e = header.after; e != header; e = e.after)
                if (value.equals(e.value))
                    return true;
        }
        return false;
    }
複製代碼

重寫父類的 containsValue(Object value) 方法,直接經過 header 遍歷鏈表判斷是否有值和 value 相等,利用雙向循環鏈表的特色進行查詢,少了對數組的外層 for 循環 ,而不用查詢 table 數組。

public void clear() {
       super.clear();
       header.before = header.after = header;
   }
複製代碼

clear() 方法先調用父類的方法 clear() 方法,以後將鏈表的 header 節點的 before 和 after 引用都指向 header 自身,即 header 節點就是一個雙向循環鏈表。這樣就沒法訪問到原鏈表中剩餘的其餘節點,他們都將被 GC 回收。清空 HashMap 的同時,將雙向鏈表還原爲只有頭結點的空鏈表。

以上即是 LinkedHashMap 源碼主要方法的分析,到這裏就要結束了,咱們來總結一下關於 HashMap 和 LinkedHashMap 的相關東西。

總結

對於 LinkedHashMap,咱們總結了如下幾點內容:

一、因爲 LinkedHashMap 繼承自 HashMap,因此它不只像 HashMap 那樣對其進行基於哈希表和單鏈表的 Entry 數組+ next 鏈表的存儲方式,並且還結合了 LinkedList 的優勢,爲每一個 Entry 節點增長了前驅和後繼,並增長了一個爲 header 頭結點,構造了一個雙向循環鏈表。(多一個以 header 爲頭結點的雙向循環鏈表,也就是說,每次 put 進來 KV,除了將其保存到對哈希表中的對應位置外,還要將其插入到雙向循環鏈表的尾部。)

二、LinkedHashMap 的屬性比 HashMap 多了一個 accessOrder 屬性。當它 false 時,表示雙向鏈表中的元素按照 Entry 插入 LinkedHashMap 到中的前後順序排序,即每次 put 到 LinkedHashMap 中的 Entry 都放在雙向鏈表的尾部,這樣遍歷雙向鏈表時,Entry 的輸出順序便和插入的順序一致,這也是默認的雙向鏈表的存儲順序;當它爲 true 時,表示雙向鏈表中的元素按照訪問的前後順序排列,能夠看到,雖然 Entry 插入鏈表的順序依然是按照其 put 到 LinkedHashMap 中的順序,但 put 和 get 方法均有調用 recordAccess 方法(put 方法在 key 相同,覆蓋原有的 Entry 的狀況下調用 recordAccess 方法), 該方法判斷 accessOrder 是否爲 true,若是是,則將當前訪問的 Entry(put 進來的 Entry 或 get 出來的 Entry)移到雙向鏈表的尾部(key 不相同時,put 新 Entry 時,會調用 addEntry,它會調用 creatEntry,該方法一樣將新插入的元素放入到雙向鏈表的尾部,既符合插入的前後順序,又符合訪問的前後順序,由於這時該 Entry 也被訪問了),不然,什麼也不作。

三、構造函數中有設置 accessOrder 的方法,若是咱們須要實現 LRU 算法時,就須要將 accessOrder 的值設定爲 TRUE。

四、在 HashMap 的 put 方法中,若是 key 不爲 null 時且哈希表中已經在存在時,循環遍歷 table[i] 中的鏈表時會調用 recordAccess 方法,而在 HashMap 中這個方法是個空方法,在LinkedHashMap中則實現了該方法,該方法會判斷 accessOrder 是否爲 true,若是爲 true,它會將當前訪問的 Entry(在這裏指 put 進來的 Entry)移動到雙向循環鏈表的尾部,從而實現雙向鏈表中的元素按照訪問順序來排序(最近訪問的 Entry 放到鏈表的最後,這樣屢次下來,前面就是最近沒有被訪問的元素,在實現 LRU 算法時,當雙向鏈表中的節點數達到最大值時,將前面的元素刪去便可,由於前面的元素是最近最少使用的),不然什麼也不作。

關於做者

專一於 Android 開發多年,喜歡寫 blog 記錄總結學習經驗,blog 同步更新於本人的公衆號,歡迎你們關注,一塊兒交流學習~

在這裏插入圖片描述
相關文章
相關標籤/搜索