LinkedHashMap(jdk1.7)瞭解一下

兩種基本順序

它是HashMap的子類,但可以保證元素按插入和訪問有序,這與TreeMap按鍵排序不同。內部還有一個雙向鏈表維護鍵值對的順序,每個鍵值對既位於哈希表中,也位於這個雙向鏈表中。支持兩種順序:一種是插入順序,另一種是訪問順序。

插入順序容易理解,先添加的在前面,後添加的在後面,修改操作不影響順序。訪問順序是指get/put操作,對一個鍵執行get/put操作,其對應的鍵值對會移到鏈表末尾,所以最末尾是最近訪問的,最開始的最久沒被訪問的,這種順序就是訪問順序。

public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder)

其中參數accessOrder就是用來指定是否按訪問順序,如果爲true,就是訪問順序。

什麼時候希望保持插入順序呢?
Map經常用來處理一些數據,其處理模式是:接受一些鍵值對作爲輸入,處理,然後輸出,輸出時希望保持原來的順序。比如一個配置文件,其中一些鍵值對形式的配置項,但其中有一些鍵是重複的,希望保留最後一個值,但還是按原來的鍵順序輸出。 再如,希望的數據模型可能就是一個Map,但希望保持添加的順序,如一個購物車,鍵爲購買項目,值爲購買數量,按用戶添加的順序保存。

什麼時候希望按訪問有序呢?
一種典型的應用LRU緩衝。緩衝是計算機技術彙總一種非常常用的技術,是一個通用的提升數據訪問性能的思路,一般用於保存常用的數據,容量小,但訪問更快。緩存是相對主存而言的,主存的容量更大,但訪問速度更慢。緩衝的基本假設是:數據多次訪問,一般訪問數據時都是先從緩衝中找,緩衝中沒有再從主存中找,找到後再放入緩衝,這樣下次如果再找相同的數據訪問就快。

緩衝的認識

緩存用於計算機技術的各個領域,比如CPU 裏有緩衝,有一級緩衝,二級緩衝,三級緩衝等,一級緩衝非常小,非常貴,也非常快,三級緩衝是相對內存而言的,它們都比內存快。

內存裏也有緩衝,內存的緩衝一般是相對硬盤數據而言得出,硬盤也可能是緩衝,緩衝網絡上其他機器的數據,比如瀏覽器訪問網頁時,會把一些網頁緩衝到本地硬盤。

一般而言,緩衝容量有限,不能無限存儲所有數據,如果緩衝滿了,當需要存儲新數據時,就需要一定的策略將一些老的數據清理出去,這個策略一般稱爲替換算法,LRU是一種流行的替換算法,它的全稱是Least Recently Used,即最近最少使用。它的思路是,最近剛被使用的很快再次被用的可能性最高,而最久沒被訪問的很快再次被用的可能性最低,所以優先清理。

protected boolean removeEldestEntry(Map.Entry<K, V> eldest){
           return false;
}

在添加元素到LinkedHashMap 後,會調用這個方法,傳遞的參數是最久沒被訪問的鍵值對,如果這個方法返回true,則這個最久的鍵值對會被刪除,Linked-HashMap的實現總是返回false,所有容量沒有限制,但子類可以重寫這個方法,滿足一定條件情況,返回true.

簡單的LRU緩衝實現

public class LRUCache<K, V> extends LinkedHashMap<K, V>{
    private int maxEntries;
    
    public LRUCache(int maxEntries){
         super(16,0.75f, true);
         this.maxEntries =maxEntries;
    }

 @Override
 protected boolean removeEldestEntry(Entry<K, V> eldest){
    return size() > maxEntries;
 }
}

基本原理

private transient Entry<K, V> header; //表示雙向鏈表的頭
private final boolean accessOrder; //accessOrder 表示訪問順序還是插入順序

private static class Entry<K, V> extends HashMap.Entry<K, V>{
Entry<K, V> before,after //指向鏈表的前驅和後繼

Entry<int hash, K key, V value, HashMap.Entry<K, V> next>{
     super(hash, key, value, next);
}

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;
}

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();
}


}

recordAccess 和recordRemoval 是HashMap.Entry中定義的方法,在HashMap中。這兩個方法的實現爲空。它們就是被設計用來被子類重寫的。在put被調用且鍵存在時,HashMap會調用Entry的recordAccess方法;在鍵被刪除時,HashMap 會調用Entry的recordRemoval方法。

LInkedHashMap.Entry 重寫了這兩個方法。在recordAccess方法中,如果是訪問順序的,則將該節點移動到鏈表的末尾;在recordRemoval方法中,將該節點從鏈表中移除。

在HashMap 的構造方法中會調用init方法,LinkedHashMap重新了該方法

void init(){
   header = new Entry<>(-1,null,null,null);

  header.before = header.after = header;
}

//會先調用 父類的addEntry方法,父類的addEntry會先調用createEntry創建節點
void addEntry(int hash, k key, V value, int bucketIndex){
  super.addEntry(hash, key, value, bucketIndex);

  Entry<K, V> eldest = header.after;

  if(removeEldestEntry(eldest)){
     removeEntryForKey(eldest.key);
  }

}

// Linked-HashMap重寫了createEntry
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++;

}

//新建節點,加入哈希表中,同時加入鏈表中,加到鏈表末尾的代碼是:
e.addBefore(header);

初始內存圖:

在這裏插入圖片描述

插入一個元素後的內存圖:
在這裏插入圖片描述

小結

用法上,它可以保持插入順序或訪問順序,插入順序經常用於處理鍵值對的數據,並保持其輸入順序,也經常用於鍵已經排好序的場景,相比TreeMap效率更高;訪問順序經常用於LRU緩衝,實現原理上,它是HashMap的子類,但內部有一個雙向鏈表維護節點的順序。

參考文章

java編程的邏輯基礎(馬俊昌)

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