兩種基本順序
它是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的子類,但內部有一個雙向鏈表維護節點的順序。