一. 基本用法
LinkedHashMap是HashMap的子類,但是內部還有一個雙向鏈表維護鍵值對的順序,每個鍵值對既位於哈希表中,也位於雙向鏈表中。LinkedHashMap支持兩種順序插入順序 、 訪問順序
1:插入順序:先添加的在前面,后添加的在后面。修改操作不影響順序
2:訪問順序:所謂訪問指的是get/put操作,對一個鍵執行get/put操作后,其對應的鍵值對會移動到鏈表末尾,所以最末尾的是最近訪問的,最開始的是最久沒有被訪問的,這就是訪問順序。
LinkedHashMap 繼承了HashMap,實現了Map接口
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap一共提供了五個構造方法:
// 構造方法1,構造一個指定初始容量和負載因子的、按照插入順序的LinkedList public LinkedHashMap(int initialCapacity, float loadFactor) { super(initialCapacity, loadFactor); accessOrder = false; } // 構造方法2,構造一個指定初始容量的LinkedHashMap,取得鍵值對的順序是插入順序 public LinkedHashMap(int initialCapacity) { super(initialCapacity); accessOrder = false; } // 構造方法3,用默認的初始化容量和負載因子創建一個LinkedHashMap,取得鍵值對的順序是插入順序 public LinkedHashMap() { super(); accessOrder = false; } // 構造方法4,通過傳入的map創建一個LinkedHashMap,容量為默認容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為默認值 public LinkedHashMap(Map<? extends K, ? extends V> m) { super(m); accessOrder = false; } // 構造方法5,根據指定容量、裝載因子和鍵值對保持順序創建一個LinkedHashMap public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
從構造方法中可以看出,默認都采用插入順序來維持取出鍵值對的次序。所有構造方法都是通過調用父類的構造方法來創建對象的。
舉個例子:鍵是按照:“c”, “d”,"a"的順序插入的,修改d不會修改順序
@Test public void test2(){ Map<String, Integer> seqMap = new LinkedHashMap<>(); seqMap.put("c",100); seqMap.put("d",200); seqMap.put("a",500); for(Entry<String,Integer> entry:seqMap.entrySet()){ System.out.println(entry.getKey()+" "+entry.getValue()); } System.out.println("---------------"); seqMap.put("d",300); for(Entry<String,Integer> entry:seqMap.entrySet()){ System.out.println(entry.getKey()+" "+entry.getValue()); } }
console輸出:
按訪問順序:
@Test public void test2(){ Map<String, Integer> seqMap = new LinkedHashMap<>(16,0.75f,true); seqMap.put("c",100); seqMap.put("d",200); seqMap.put("a",500); for(Entry<String,Integer> entry:seqMap.entrySet()){ System.out.println(entry.getKey()+" "+entry.getValue()); } System.out.println("---------------"); seqMap.put("d",300); for(Entry<String,Integer> entry:seqMap.entrySet()){ System.out.println(entry.getKey()+" "+entry.getValue()); } }
console輸出:
二:HashMap與LinkedHashMap的結構對比
LinkedHashMap其實就是可以看成HashMap的基礎上,多了一個雙向鏈表來維持順序。
注意該循環雙向鏈表的頭部存放的是最久訪問的節點或最先插入的節點,尾部為最近訪問的或最近插入的節點,迭代器遍歷方向是從鏈表的頭部開始到鏈表尾部結束,在鏈表尾部有一個空的header節點,該節點不存放key-value內容,為LinkedHashMap類的成員屬性,循環雙向鏈表的入口。
三:借用 LinkedHashMap實現最近被使用(LRU)緩存
最近最少使用緩存的回收
為了實現緩存回收,我們需要很容易做到:
- 查詢出最近最晚使用的項
- 給最近使用的項做一個標記
鏈表可以實現這兩個操作。檢測最近最少使用的項只需要返回鏈表的尾部。標記一項為最近使用的項只需要從當前位置移除,然后將該項放置到頭部。比較困難的事情是怎么快速的在鏈表中找到該項。
對於使用鏈表這種方法,put 和 get 都需要遍歷鏈表查找數據是否存在,所以時間復雜度為 O(n)。空間復雜度為 O(1)。
空間換時間
在實際的應用中,當我們要去讀取一個數據的時候,會先判斷該數據是否存在於緩存器中,如果存在,則返回,如果不存在,則去別的地方查找該數據(例如磁盤),找到后再把該數據存放於緩存器中,再返回。
所以在實際的應用中,put 操作一般伴隨着 get 操作,也就是說,get 操作的次數是比較多的,而且命中率也是相對比較高的,進而 put 操作的次數是比較少的,我們我們是可以考慮采用空間換時間的方式來加快我們的 get 的操作的。
例如我們可以用一個額外哈希表(例如HashMap)來存放 key-value,這樣的話,我們的 get 操作就可以在 O(1) 的時間內尋找到目標節點,並且把 value 返回了。
然而,大家想一下,用了哈希表之后,get 操作真的能夠在 O(1) 時間內完成嗎?
用了哈希表之后,雖然我們能夠在 O(1) 時間內找到目標元素,可以,我們還需要刪除該元素,並且把該元素插入到鏈表頭部啊,刪除一個元素,我們是需要定位到這個元素的前驅的,然而定位到這個元素的前驅,是需要 O(n) 時間復雜度的。
最后的結果是,用了哈希表時候,最壞時間復雜度還是 O(1),而空間復雜度也變為了 O(n)。
雙向鏈表+哈希表
我們都已經能夠在 O(1) 時間復雜度找到要刪除的節點了,之所以還得花 O(n) 時間復雜度才能刪除,主要是時間是花在了節點前驅的查找上,為了解決這個問題,其實,我們可以把單鏈表換成雙鏈表,這樣的話,我們就可以很好着解決這個問題了,而且,換成雙鏈表之后,你會發現,它要比單鏈表的操作簡單多了。
所以我們最后的方案是:雙鏈表 + 哈希表,采用這兩種數據結構的組合,我們的 get 操作就可以在 O(1) 時間復雜度內完成了。由於 put 操作我們要刪除的節點一般是尾部節點,所以我們可以用一個變量 tai 時刻記錄尾部節點的位置,這樣的話,我們的 put 操作也可以在 O(1) 時間內完成了。
Java已經為我們提供了這種形式的數據結構 LinkedHashMap!它甚至提供可覆蓋回收策略的方法(見removeEldestEntry文檔)。唯一需要我們注意的事情是,改鏈表的順序是插入的順序,而不是訪問的順序。但是,有一個構造函數提供了一個選項,可以使用訪問的順序
import java.util.LinkedHashMap; import java.util.Map; public LRUCache<K, V> extends LinkedHashMap<K, V> { private int cacheSize; public LRUCache(int cacheSize) { super(16, 0.75, true); this.cacheSize = cacheSize; } //LinkedHashMap有一個removeEldestEntry(Map.Entry eldest)方法,通過覆蓋這個方法,加入一定的條件,滿足條件返回true。當put進新的值方法返回true時,便移除該map中最老的鍵和值。 protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() >= cacheSize; } }
注:在LinkedHashMap添加元素后,會調用removeEldestEntry防范,傳遞的參數時最久沒有被訪問的鍵值對,如果方法返回true,這個最久的鍵值對就會被刪除。LinkedHashMap中的實現總返回false,該子類重寫后即可實現對容量的控制
自己通過HashMap+雙向鏈表實現LRU緩存算法
import java.util.HashMap; public class LRUCache<K, V> { private int currentCacheSize; // 當前緩存的容量 private int CacheCapcity; // 緩存容量最大值 private HashMap<K,CacheNode> caches; //HashMap private CacheNode first; //鏈表頭 private CacheNode last; //鏈表尾 public LRUCache(int size) { this.currentCacheSize = 0; this.CacheCapcity = size; caches = new HashMap<K, CacheNode>(size); } public void put(K k,V v){ CacheNode node = caches.get(k); if(node == null) { //緩存中沒有該key if(caches.size() >= CacheCapcity) { //緩存容量已經達到最大值了,不能裝了 caches.remove(last.key); //刪除HashMap中的Node removeLast(); //刪除雙向鏈表中的尾結點Node } node = new CacheNode(); node.key = k; } node.value = v; moveToFirst(node); caches.put(k, node); } public Object get(K k){ CacheNode node = caches.get(k); if(node == null) { return null; } moveToFirst(node); return node.value; } public Object remove(K k) { CacheNode node = caches.get(k); if(node != null) { if(node.pre != null){ node.pre.next=node.next; } if(node.next != null){ node.next.pre=node.pre; } if(node == first){ first = node.next; } if(node == last){ last = node.pre; } } return null; } public void clear(){ first = null; last = null; caches.clear(); } private void removeLast(){ if(last != null) { last = last.pre; if(last == null) { first = null; }else { last.next = null; } } } /** * @param node 插入的結點</br> * put數據,將新數據放到鏈表頭部,這樣鏈表頭部就是最新的數據,尾部就是最少訪問的數據 */ private void moveToFirst(CacheNode node) { if(first == node){ return; } if(node.next != null){ node.next.pre = node.pre; } if(node.pre != null){ node.pre.next = node.next; } if(node == last){ last= last.pre; } if(first == null || last == null){ first = last = node; return; } node.next=first; first.pre = node; first = node; first.pre=null; } @Override public String toString(){ StringBuilder sb = new StringBuilder(); CacheNode node = first; while(node != null){ sb.append(String.format("%s:%s ", node.key,node.value)); node = node.next; } return sb.toString(); } public static void main(String[] args) { LRUCache<Integer,String> lru = new LRUCache<Integer,String>(3); lru.put(1, "a"); // 1:a System.out.println(lru.toString()); lru.put(2, "b"); // 2:b 1:a System.out.println(lru.toString()); lru.put(3, "c"); // 3:c 2:b 1:a System.out.println(lru.toString()); lru.put(4, "d"); // 4:d 3:c 2:b System.out.println(lru.toString()); lru.put(1, "aa"); // 1:aa 4:d 3:c System.out.println(lru.toString()); lru.put(2, "bb"); // 2:bb 1:aa 4:d System.out.println(lru.toString()); lru.put(5, "e"); // 5:e 2:bb 1:aa System.out.println(lru.toString()); lru.get(1); // 1:aa 5:e 2:bb System.out.println(lru.toString()); lru.remove(11); // 1:aa 5:e 2:bb System.out.println(lru.toString()); lru.remove(1); //5:e 2:bb System.out.println(lru.toString()); lru.put(1, "aaa"); //1:aaa 5:e 2:bb System.out.println(lru.toString()); } }