Java集合(六)LinkedHashMap詳解

本文目錄

1 LinkedHashMap概述

2 LinkedHashMap 源碼定義

2.1 類結構定義

2.2 成員變量定義

2.3 成員方法定義

2.4 基本元素 Entry

3 LinkedHashMap的方法

3.1 LinkedHashMap的構造方法

3.2 初始化方法init方法

3.3 LinkedHashMap的快速存取

3.3.1 LinkedHashMap 的存儲實現 : put(key, vlaue)

3.3.2 LinkedHashMap 的擴容操作 : resize()

3.3.3 LinkedHashMap 的讀取實現 :get(Object key)

3.3.4 LinkedHashMap 存取小結

4 LinkedHashMap與LRU算法

4.1 put操作與標誌位accessOrder

4.2 get操作與標誌位accessOrder

4.3 LinkedListMap與LRU小結

5 使用LinkedHashMap實現LRU算法

5.1 使用LinkedHashMap實現LRU算法示例

5.2 LinkedHashMap 有序性原理分析

6 JDK1.8的修改

7 LinkedHashMap小結


1 LinkedHashMap概述

HashMap 是 Java Collection Framework 的重要成員,也是Map族(如下圖所示)中我們最爲常用的一種。不過遺憾的是,HashMap是無序的,也就是說,迭代HashMap所得到的元素順序並不是它們最初放置到HashMap的順序。

  HashMap的這一缺點往往會造成諸多不便,因爲在有些場景中,我們確需要用到一個可以保持插入順序的Map。慶幸的是,JDK爲我們解決了這個問題,它爲HashMap提供了一個子類 —— LinkedHashMap。雖然LinkedHashMap增加了時間和空間上的開銷,但是它通過維護一個額外的雙向鏈表保證了迭代順序。   

  特別地,該迭代順序可以是插入順序,也可以是訪問順序。因此,根據鏈表中元素的順序可以將LinkedHashMap分爲:保持插入順序的LinkedHashMap和保持訪問順序的LinkedHashMap,其中LinkedHashMap的默認實現是按插入順序排序的。

 LinkedHashMap與Map接口之間的繼承關係如下圖所示:

本質上,HashMap和雙向鏈表合二爲一即是LinkedHashMap。所謂LinkedHashMap,其落腳點在HashMap,因此更準確地說,它是一個將所有Entry節點鏈入一個雙向鏈表的HashMap。

  在LinkedHashMapMap中,所有put進來的Entry都保存在如下面第一個圖所示的哈希表中,但由於它又額外定義了一個以head爲頭結點的雙向鏈表(如下面第二個圖所示),因此對於每次put進來Entry,除了將其保存到哈希表中對應的位置上之外,還會將其插入到雙向鏈表的尾部。

  更直觀地,下圖很好地還原了LinkedHashMap的原貌:HashMap和雙向鏈表的密切配合和分工合作造就了LinkedHashMap。特別需要注意的是,next用於維護HashMap各個桶中的Entry鏈,before、after用於維護LinkedHashMap的雙向鏈表,雖然它們的作用對象都是Entry,但是各自分離,是兩碼事兒。

     其中,HashMap與LinkedHashMap的Entry結構示意圖如下圖所示:

  特別地,由於LinkedHashMap是HashMap的子類,所以LinkedHashMap自然會擁有HashMap的所有特性。比如,LinkedHashMap也最多隻允許一條Entry的鍵爲Null(多條會覆蓋),但允許多條Entry的值爲Null。此外,LinkedHashMap 也是 Map 的一個非同步的實現。此外,LinkedHashMap還可以用來實現LRU (Least recently used, 最近最少使用)算法,這個問題會在下文的特別談到。

2 LinkedHashMap 源碼定義

2.1 類結構定義

LinkedHashMap繼承於HashMap,其在JDK中的定義爲:

public class LinkedHashMap<K,V> extends HashMap<K,V>
    implements Map<K,V> {

    ...
}

2.2 成員變量定義

與HashMap相比,LinkedHashMap增加了兩個屬性用於保證迭代順序,分別是雙向鏈表頭結點header 和 標誌位accessOrder (值爲true時,表示按照訪問順序迭代;值爲false時,表示按照插入順序迭代)。

/**
 * 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.
 *
 * @serial
 */
private final boolean accessOrder;  //true表示按照訪問順序迭代,false時表示按照插入順序 

2.3 成員方法定義

  從下圖我們可以看出,LinkedHashMap中並增加沒有額外方法。也就是說,LinkedHashMap與HashMap在操作上大致相同,只是在實現細節上略有不同罷了。

2.4 基本元素 Entry

  LinkedHashMap採用的hash算法和HashMap相同,但是它重新定義了Entry。LinkedHashMap中的Entry增加了兩個指針 before 和 after,它們分別用於維護雙向鏈接列表。特別需要注意的是,next用於維護HashMap各個桶中Entry的連接順序,before、after用於維護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與LinkedHashMap的Entry結構示意圖如下圖所示:

3 LinkedHashMap的方法

3.1 LinkedHashMap的構造方法

 LinkedHashMap 一共提供了五個構造函數,它們都是在HashMap的構造函數的基礎上實現的,除了默認空參數構造方法,下面這個構造函數包含了大部分其他構造方法使用的參數,就不一一列舉了。

LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)

 該構造函數意在構造一個指定初始容量和指定負載因子的具有指定迭代順序的LinkedHashMap,其源碼如下:

/**
     * Constructs an empty LinkedHashMap instance with the
     * specified initial capacity, load factor and ordering mode.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @param  accessOrder     the ordering mode - true for
     *         access-order, false 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);   // 調用HashMap對應的構造函數
        this.accessOrder = accessOrder;    // 迭代順序的默認值
    }

初始容量和負載因子是影響HashMap性能的兩個重要參數。同樣地,它們也是影響LinkedHashMap性能的兩個重要參數。此外,LinkedHashMap 增加了雙向鏈表頭結點 header和標誌位 accessOrder兩個屬性用於保證迭代順序。

LinkedHashMap(Map<? extends K, ? extends V> m)

該構造函數意在構造一個與指定 Map 具有相同映射的 LinkedHashMap,其 初始容量不小於 16 (具體依賴於指定Map的大小),負載因子是 0.75,是 Java Collection Framework 規範推薦提供的,其源碼如下:

/**
 * Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with
 * the same mappings as the specified map.  The <tt>LinkedHashMap</tt>
 * instance is created with a default load factor (0.75) and an initial
 * capacity sufficient to hold the mappings in the specified map.
 *
 * @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);       // 調用HashMap對應的構造函數
    accessOrder = false;    // 迭代順序的默認值
}

3.2 初始化方法init方法

從上面的五種構造函數我們可以看出,無論採用何種方式創建LinkedHashMap,其都會調用HashMap相應的構造函數。事實上,不管調用HashMap的哪個構造函數,HashMap的構造函數都會在最後調用一個init()方法進行初始化,只不過這個方法在HashMap中是一個空實現,而在LinkedHashMap中重寫了它用於初始化它所維護的雙向鏈表。例如,HashMap的參數爲空的構造函數以及init方法的源碼如下:

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
    table = new Entry[DEFAULT_INITIAL_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() {
    }  

在LinkedHashMap中,它重寫了init方法以便初始化雙向列表,源碼如下:

 /**
     * Called by superclass constructors and pseudoconstructors (clone,
     * readObject) before any entries are inserted into the map.  Initializes
     * the chain.
     */
    void init() {
        header = new Entry<K,V>(-1, null, null, null);
        header.before = header.after = header;
    }

因此,我們在創建LinkedHashMap的同時就會不知不覺地對雙向鏈表進行初始化。

3.3 LinkedHashMap的快速存取

我們知道,在HashMap中最常用的兩個操作就是:put(Key,Value) 和 get(Key)。同樣地,在 LinkedHashMap 中最常用的也是這兩個操作。

對於put(Key,Value)方法而言,LinkedHashMap完全繼承了HashMap的 put(Key,Value) 方法,只是對put(Key,Value)方法所調用的recordAccess方法和addEntry方法進行了重寫;對於get(Key)方法而言,LinkedHashMap則直接對它進行了重寫。

下面我們結合JDK源碼看 LinkedHashMap 的存取實現。

3.3.1 LinkedHashMap 的存儲實現 : put(key, vlaue)

LinkedHashMap沒有對 put(key,vlaue) 方法進行任何直接的修改,完全繼承了HashMap的 put(Key,Value) 方法,其源碼如下:

public V put(K key, V value) {

    //當key爲null時,調用putForNullKey方法,並將該鍵值對保存到table的第一個位置 
    if (key == null)
        return putForNullKey(value); 

    //根據key的hashCode計算hash值
    int hash = hash(key.hashCode());           

    //計算該鍵值對在數組中的存儲位置(哪個桶)
    int i = indexFor(hash, table.length);              

    //在table的第i個桶上進行迭代,尋找 key 保存的位置
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;
        //判斷該條鏈上是否存在hash值相同且key值相等的映射,若存在,則直接覆蓋 value,並返回舊value
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this); // LinkedHashMap重寫了Entry中的recordAccess方法--- (1)    
            return oldValue;    // 返回舊值
        }
    }

    modCount++; //修改次數增加1,快速失敗機制

    //原Map中無該映射,將該添加至該鏈的鏈頭
    addEntry(hash, key, value, i);  // LinkedHashMap重寫了HashMap中的createEntry方法 ---- (2)    
    return null;
}

上述源碼反映了LinkedHashMap與HashMap保存數據的過程。特別地,在LinkedHashMap中,它對addEntry方法和Entry的recordAccess方法進行了重寫。下面我們對比地看一下LinkedHashMap 和HashMap的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.
 *
 * LinkedHashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {   

    //創建新的Entry,並插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個有效節點(header後的那個節點)爲最近最少使用的節點,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要,則刪除掉該近期最少使用的節點,  
    //這要看對removeEldestEntry的覆寫,由於默認爲false,因此默認是不做任何處理的。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 


 /**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 * 
 * HashMap中的addEntry方法
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    //獲取bucketIndex處的Entry
    Entry<K,V> e = table[bucketIndex];

    //將新創建的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry 
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);

    //若HashMap中元素的個數超過極限了,則容量擴大兩倍
    if (size++ >= threshold)
        resize(2 * table.length);
}

由於LinkedHashMap本身維護了插入的先後順序,因此其可以用來做緩存,14~19行的操作就是用來支持LRU算法的,這裏暫時不用去關心它。此外,在LinkedHashMap的addEntry方法中,它重寫了HashMap中的createEntry方法,我們接着看一下createEntry方法:

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,這點與HashMap中相同 
    //創建新的Entry並將其鏈入到數組對應桶的鏈表的頭結點處, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時,都會將其插入到雙向鏈表的尾部,  
    //這樣就按照Entry插入LinkedHashMap的先後順序來迭代元素(LinkedHashMap根據雙向鏈表重寫了迭代器)
    //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,也符合LRU算法的實現  
    e.addBefore(header);  
    size++;  
}  

由以上源碼我們可以知道,在LinkedHashMap中向哈希表中插入新Entry的同時,還會通過Entry的addBefore方法將其鏈入到雙向鏈表中。其中,addBefore方法本質上是一個雙向鏈表的插入操作,其源碼如下:

//在雙向鏈表中,將當前的Entry插入到existingEntry(header)的前面  
private void addBefore(Entry<K,V> existingEntry) {  
    after  = existingEntry;  
    before = existingEntry.before;  
    before.after = this;  
    after.before = this;  
}  

總的來說,相比HashMap而言,LinkedHashMap在向哈希表添加一個鍵值對的同時,也會將其鏈入到它所維護的雙向鏈表中,以便設定迭代順序。

3.3.2 LinkedHashMap 的擴容操作 : resize()

在HashMap中,我們知道隨着HashMap中元素的數量越來越多,發生碰撞的概率將越來越大,所產生的子鏈長度就會越來越長,這樣勢必會影響HashMap的存取速度。爲了保證HashMap的效率,系統必須要在某個臨界點進行擴容處理,該臨界點就是HashMap中元素的數量在數值上等於threshold(table數組長度*加載因子)。

但是,擴容是一個非常耗時的過程,因爲它需要重新計算這些元素在新table數組中的位置並進行復制處理。所以,如果我們能夠提前預知HashMap中元素的個數,那麼在構造HashMap時預設元素的個數能夠有效的提高HashMap的性能。同樣的問題也存在於LinkedHashMap中,因爲LinkedHashMap本來就是一個HashMap,只是它還將所有Entry節點鏈入到了一個雙向鏈表中。LinkedHashMap完全繼承了HashMap的resize()方法,只是對它所調用的transfer方法進行了重寫。我們先看resize()方法源碼:

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;

    // 若 oldCapacity 已達到最大值,直接將 threshold 設爲 Integer.MAX_VALUE
    if (oldCapacity == MAXIMUM_CAPACITY) {  
        threshold = Integer.MAX_VALUE;
        return;             // 直接返回
    }

    // 否則,創建一個更大的數組
    Entry[] newTable = new Entry[newCapacity];

    //將每條Entry重新哈希到新的數組中
    transfer(newTable);  //LinkedHashMap對它所調用的transfer方法進行了重寫

    table = newTable;
    threshold = (int)(newCapacity * loadFactor);  // 重新設定 threshold
}

從上面代碼中我們可以看出,Map擴容操作的核心在於重哈希。所謂重哈希是指重新計算原HashMap中的元素在新table數組中的位置並進行復制處理的過程。鑑於性能和LinkedHashMap自身特點的考量,LinkedHashMap對重哈希過程(transfer方法)進行了重寫,源碼如下:

/**
 * Transfers all entries to new table array.  This method is called
 * by superclass resize.  It is overridden for performance, as it is
 * faster to iterate using our linked list.
 */
void transfer(HashMap.Entry[] newTable) {
    int newCapacity = newTable.length;
    // 與HashMap相比,藉助於雙向鏈表的特點進行重哈希使得代碼更加簡潔
    for (Entry<K,V> e = header.after; e != header; e = e.after) {
        int index = indexFor(e.hash, newCapacity);   // 計算每個Entry所在的桶
        // 將其鏈入桶中的鏈表
        e.next = newTable[index];
        newTable[index] = e;   
    }
}

如上述源碼所示,LinkedHashMap藉助於自身維護的雙向鏈表輕鬆地實現了重哈希操作。

3.3.3 LinkedHashMap 的讀取實現 :get(Object key)

  相對於LinkedHashMap的存儲而言,讀取就顯得比較簡單了。LinkedHashMap中重寫了HashMap中的get方法,源碼如下:

public V get(Object key) {
    // 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

/**
     * Returns the entry associated with the specified key in the
     * HashMap.  Returns null if the HashMap contains no mapping
     * for the key.
     * 
     * HashMap 中的方法
     *     
     */
    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }

        int hash = (key == null) ? 0 : hash(key);
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        }
        return null;
    }

在LinkedHashMap的get方法中,通過HashMap中的getEntry方法獲取Entry對象。注意這裏的recordAccess方法,如果鏈表中元素的排序規則是按照插入的先後順序排序的話,該方法什麼也不做;如果鏈表中元素的排序規則是按照訪問的先後順序排序的話,則將e移到鏈表的末尾處。

另外,同樣地,調用LinkedHashMap的get(Object key)方法後,若返回值是 NULL,則也存在如下兩種可能:

(1)該 key 對應的值就是 null;

(2)HashMap 中不存在該 key。

3.3.4 LinkedHashMap 存取小結

(1)LinkedHashMap的存取過程基本與HashMap基本類似,只是在細節實現上稍有不同,這是由LinkedHashMap本身的特性所決定的,因爲它要額外維護一個雙向鏈表用於保持迭代順序。

(2)在put操作上,雖然LinkedHashMap完全繼承了HashMap的put操作,但是在細節上還是做了一定的調整,比如,在LinkedHashMap中向哈希表中插入新Entry的同時,還會通過Entry的addBefore方法將其鏈入到雙向鏈表中。

(3)在擴容操作上,雖然LinkedHashMap完全繼承了HashMap的resize操作,但是鑑於性能和LinkedHashMap自身特點的考量,LinkedHashMap對其中的重哈希過程(transfer方法)進行了重寫。

(4)在讀取操作上,LinkedHashMap中重寫了HashMap中的get方法,通過HashMap中的getEntry方法獲取Entry對象。在此基礎上,進一步獲取指定鍵對應的值。

4 LinkedHashMap與LRU算法

LRU算法即Least recently used(最近最少使用)算法。

4.1 put操作與標誌位accessOrder

// 將key/value添加到LinkedHashMap中      
public V put(K key, V value) {      
    // 若key爲null,則將該鍵值對添加到table[0]中。      
    if (key == null)      
        return putForNullKey(value);      
    // 若key不爲null,則計算該key的哈希值,然後將其添加到該哈希值對應的鏈表中。      
    int hash = hash(key.hashCode());      
    int i = indexFor(hash, table.length);      
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {      
        Object k;      
        // 若key對已經存在,則用新的value取代舊的value     
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {      
            V oldValue = e.value;      
            e.value = value;      
            e.recordAccess(this);      
            return oldValue;      
        }      
    }      

    // 若key不存在,則將key/value鍵值對添加到table中      
    modCount++;    
    //將key/value鍵值對添加到table[i]處    
    addEntry(hash, key, value, i);      
    return null;      
}    

從上述源碼我們可以看到,當要put進來的Entry的key在哈希表中已經在存在時,會調用Entry的recordAccess方法;當該key不存在時,則會調用addEntry方法將新的Entry插入到對應桶的單鏈表的頭部。我們先來看recordAccess方法:

/**
* 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;  
    //如果鏈表中元素按照訪問順序排序,則將當前訪問的Entry移到雙向循環鏈表的尾部,  
    //如果是按照插入的先後順序排序,則不做任何事情。  
    if (lm.accessOrder) {  
        lm.modCount++;  
        //移除當前訪問的Entry  
        remove();  
        //將當前訪問的Entry插入到鏈表的尾部  
        addBefore(lm.header);  
      }  
  } 

LinkedHashMap重寫了HashMap中的recordAccess方法(HashMap中該方法爲空),當調用父類的put方法時,在發現key已經存在時,會調用該方法;當調用自己的get方法時,也會調用到該方法。

該方法提供了LRU算法的實現,它將最近使用的Entry放到雙向循環鏈表的尾部。也就是說,當accessOrder爲true時,get方法和put方法都會調用recordAccess方法使得最近使用的Entry移到雙向鏈表的末尾;當accessOrder爲默認值false時,從源碼中可以看出recordAccess方法什麼也不會做。我們反過頭來,再看一下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.
     *
     * LinkedHashMap中的addEntry方法
     */
    void addEntry(int hash, K key, V value, int bucketIndex) { 
        //創建新的Entry,並插入到LinkedHashMap中  
    createEntry(hash, key, value, bucketIndex);  // 重寫了HashMap中的createEntry方法

    //雙向鏈表的第一個有效節點(header後的那個節點)爲最近最少使用的節點,這是用來支持LRU算法的
    Entry<K,V> eldest = header.after;  
    //如果有必要,則刪除掉該近期最少使用的節點,  
    //這要看對removeEldestEntry的覆寫,由於默認爲false,因此默認是不做任何處理的。  
    if (removeEldestEntry(eldest)) {  
        removeEntryForKey(eldest.key);  
    } else {  
        //擴容到原來的2倍  
        if (size >= threshold)  
            resize(2 * table.length);  
    }  
} 

void createEntry(int hash, K key, V value, int bucketIndex) { 
    // 向哈希表中插入Entry,這點與HashMap中相同 
    //創建新的Entry並將其鏈入到數組對應桶的鏈表的頭結點處, 
    HashMap.Entry<K,V> old = table[bucketIndex];  
    Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
    table[bucketIndex] = e;     

    //在每次向哈希表插入Entry的同時,都會將其插入到雙向鏈表的尾部,  
    //這樣就按照Entry插入LinkedHashMap的先後順序來迭代元素(LinkedHashMap根據雙向鏈表重寫了迭代器)
    //同時,新put進來的Entry是最近訪問的Entry,把其放在鏈表末尾 ,也符合LRU算法的實現  
    e.addBefore(header);  
    size++;  
}  

同樣是將新的Entry鏈入到table中對應桶中的單鏈表中,但可以在createEntry方法中看出,同時也會把新put進來的Entry插入到了雙向鏈表的尾部。 從插入順序的層面來說,新的Entry插入到雙向鏈表的尾部可以實現按照插入的先後順序來迭代Entry,而從訪問順序的層面來說,新put進來的Entry又是最近訪問的Entry,也應該將其放在雙向鏈表的尾部。在上面的addEntry方法中還調用了removeEldestEntry方法,該方法源碼如下:

 /* <p>This implementation merely returns <tt>false</tt> (so that this
 * map acts like a normal map - the eldest element is never removed).
 *
 * @param    eldest The least recently inserted entry in the map, or if
 *           this is an access-ordered map, the least recently accessed
 *           entry.  This is the entry that will be removed it this
 *           method returns <tt>true</tt>.  If the map was empty prior
 *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
 *           in this invocation, this will be the entry that was just
 *           inserted; in other words, if the map contains a single
 *           entry, the eldest entry is also the newest.
 * @return   <tt>true</tt> if the eldest entry should be removed
 *           from the map; <tt>false</tt> if it should be retained.
 */
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false;
}

該方法是用來被重寫的,一般地,如果用LinkedHashMap實現LRU算法,就要重寫該方法。比如可以將該方法覆寫爲如果設定的內存已滿,則返回true,這樣當再次向LinkedHashMap中putEntry時,在調用的addEntry方法中便會將近期最少使用的節點刪除掉(header後的那個節點)。

4.2 get操作與標誌位accessOrder

在LinkedHashMap中進行讀取操作時,一樣也會調用recordAccess方法。

public V get(Object key) {
    // 根據key獲取對應的Entry,若沒有這樣的Entry,則返回null
    Entry<K,V> e = (Entry<K,V>)getEntry(key); 
    if (e == null)      // 若不存在這樣的Entry,直接返回
        return null;
    e.recordAccess(this);
    return e.value;
}

4.3 LinkedListMap與LRU小結

  使用LinkedHashMap實現LRU的必要前提是將accessOrder標誌位設爲true以便開啓按訪問順序排序的模式。我們可以看到,無論是put方法還是get方法,都會導致目標Entry成爲最近訪問的Entry,因此就把該Entry加入到了雙向鏈表的末尾:get方法通過調用recordAccess方法來實現;

put方法在覆蓋已有key的情況下,也是通過調用recordAccess方法來實現,在插入新的Entry時,則是通過createEntry中的addBefore方法來實現。這樣,我們便把最近使用的Entry放入到了雙向鏈表的後面。多次操作後,雙向鏈表前面的Entry便是最近沒有使用的,這樣當節點個數滿的時候,刪除最前面的Entry(head後面的那個Entry)即可,因爲它就是最近最少使用的Entry。

5 使用LinkedHashMap實現LRU算法

5.1 使用LinkedHashMap實現LRU算法示例

以下使用LinkedHashMap實現一個符合LRU算法的數據結構,該結構最多可以緩存6個元素,但元素多餘六個時,會自動刪除最近最久沒有被使用的元素,如下所示:

public class LRU<K,V> extends LinkedHashMap<K, V> implements Map<K, V>{

    private static final long serialVersionUID = 1L;

    public LRU(int initialCapacity,
             float loadFactor,
                        boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

    /** 
     * @description 重寫LinkedHashMap中的removeEldestEntry方法,當LRU中元素多餘6個時,
     *              刪除最不經常使用的元素
     */  
    @Override
    protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
        // TODO Auto-generated method stub
        if(size() > 6){
            return true;
        }
        return false;
    }

    public static void main(String[] args) {

        LRU<Character, Integer> lru = new LRU<Character, Integer>(
                16, 0.75f, true);

        String s = "abcdefghijkl";
        for (int i = 0; i < s.length(); i++) {
            lru.put(s.charAt(i), i);
        }
        System.out.println("LRU中key爲h的Entry的值爲: " + lru.get('h'));
        System.out.println("LRU的大小 :" + lru.size());
        System.out.println("LRU :" + lru);
    }
}

下圖是程序的運行結果示例: 5.2 

5.2 LinkedHashMap 有序性原理分析

如前文所述,LinkedHashMap 增加了雙向鏈表頭結點header 和 標誌位accessOrder兩個屬性用於保證迭代順序。但是要想真正實現其有序性,還差臨門一腳,那就是重寫HashMap 的迭代器,其源碼實現如下:

private abstract class LinkedHashIterator<T> implements Iterator<T> {
    Entry<K,V> nextEntry    = header.after;
    Entry<K,V> lastReturned = null;

    /**
     * The modCount value that the iterator believes that the backing
     * List should have.  If this expectation is violated, the iterator
     * has detected concurrent modification.
     */
    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;
    }
}

// Key 迭代器,KeySet
private class KeyIterator extends LinkedHashIterator<K> {   
    public K next() { return nextEntry().getKey(); }
}

   // Value 迭代器,Values(Collection)
private class ValueIterator extends LinkedHashIterator<V> {
    public V next() { return nextEntry().value; }
}

// Entry 迭代器,EntrySet
private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() { return nextEntry(); }
}

從上述代碼中我們可以知道,LinkedHashMap重寫了HashMap 的迭代器,它使用其維護的雙向鏈表進行迭代輸出。

6 JDK1.8的修改

原文是基於JDK1.6的實現,實際上JDK1.8對其進行了改動。 首先它刪除了addentry,createenrty等方法(事實上是hashmap的改動影響了它而已)。Linkedhashmap同樣使用了大部分hashmap的增刪改查方法。 新版本linkedhashmap主要是通過對hashmap內置幾個方法重寫來實現LRU的。

Hashmap不提供實現:

void afterNodeAccess(Node<K,V> p) { }

void afterNodeInsertion(boolean evict) { }

void afterNodeRemoval(Node<K,V> p) { }

LinkedHashMap的實現:

(1)處理元素被訪問後的情況

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

(2)處理元素插入後的情況

  void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }

(3)處理元素被刪除後的情況

void afterNodeRemoval(Node<K,V> e) { // unlink
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}
}

另外1.8的HashMap在鏈表長度超過8時自動轉爲紅黑樹,會按順序插入鏈表中的元素,可以自定義比較器來定義節點的插入順序。

1.8的LinkedHashMap同樣會使用這一特性,當變爲紅黑樹以後,節點的先後順序同樣是插入紅黑樹的順序,其雙向鏈表的性質沒有改表,只是原來hashmap的鏈表變成了紅黑樹而已,在此不要混淆。

7 LinkedHashMap小結

(1) LinkedHashMap在HashMap的數組加鏈表結構的基礎上,將所有節點連成了一個雙向鏈表。

(2) 當主動傳入的accessOrder參數爲false時, 使用put方法時,新加入元素不會被加入雙向鏈表,get方法使用時也不會把元素放到雙向鏈表尾部。

(3) 當主動傳入的accessOrder參數爲true時,使用put方法新加入的元素,如果遇到了哈希衝突,並且對key值相同的元素進行了替換,就會被放在雙向鏈表的尾部,當元素超過上限且removeEldestEntry方法返回true時,直接刪除最早元素以便新元素插入。如果沒有衝突直接放入,同樣加入到鏈表尾部。使用get方法時會把get到的元素放入雙向鏈表尾部。

(4) LinkedHashMap的擴容比HashMap來的方便,因爲HashMap需要將原來的每個鏈表的元素分別在新數組進行反向插入鏈化,而LinkedHashMap的元素都連在一個鏈表上,可以直接迭代然後插入。

(5) LinkedHashMap的removeEldestEntry方法默認返回false,要實現LRU很重要的一點就是集合滿時要將最久未訪問的元素刪除,在LinkedHashMap中這個元素就是頭指針指向的元素。實現LRU可以直接實現繼承LinkedHashMap並重寫removeEldestEntry方法來設置緩存大小。JDK中實現了LRUCache也可以直接使用。

 

附言:

本文整理來源於網絡、博客等資源,僅做個人學習筆記複習所用。

如果對你學習有用,請點贊共同學習!

如有侵權,請聯繫我修改或刪除!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章