先看再點贊,給本身一點思考的時間,微信搜索【 沉默王二】關注這個有顏值卻僞裝靠才華苟且的程序員。
本文 GitHub github.com/itwanger 已收錄,裏面還有我精心爲你準備的一線大廠面試題。
同窗們好啊,還記得 HashMap 那篇嗎?我本身感受寫得很是棒啊,既通俗易懂,又深刻源碼,真的是分析得透透徹徹、清清楚楚、明明白白的。(一不當心又上仨成語?)HashMap 哪哪都好,真的,只要你想用鍵值對,第一時間就應該想到它。java
但俗話說了,「金無足赤人無完人」,HashMap 也不例外。有一種需求它就知足不了,假如咱們須要一個按照插入順序來排列的鍵值對集合,那 HashMap 就無能爲力了。由於爲了提升查找效率,HashMap 在插入的時候對鍵作了一次哈希算法,這就致使插入的元素是無序的。node
對這一點還不太明白的同窗,能夠再回到 HashMap 那一篇,看看我對 put()
方法的講解。git
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i; // ①、數組 table 爲 null 時,調用 resize 方法建立默認大小的數組 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // ②、計算下標,若是該位置上沒有值,則填充 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); }
這個公式 i = (n - 1) & hash
計算後的值並非按照 0、一、二、三、四、5 這樣有序的下標將鍵值對插入到數組當中的,而是有必定的隨機性。程序員
那 LinkedHashMap 就是爲這個需求應運而生的。LinkedHashMap 繼承了 HashMap,因此 HashMap 有的關於鍵值對的功能,它也有了。github
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{}
此外,LinkedHashMap 內部又追加了雙向鏈表,來維護元素的插入順序。注意下面代碼中的 before 和 after,它倆就是用來維護當前元素的前一個元素和後一個元素的順序的。面試
static class Entry<K,V> extends HashMap.Node<K,V> { LinkedHashMap.Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Node<K,V> next) { super(hash, key, value, next); } }
關於雙向鏈表,同窗們能夠回頭看一遍我寫的 LinkedList 那篇文章,會對理解本篇的 LinkedHashMap 有很大的幫助。算法
在繼續下面的內容以前,我先貼一張圖片,給你們增添一點樂趣——看我這心操的。 UUID 那篇文章的標題裏用了「好笑」和「你」,結果就看到了下面這麼樂呵的留言。數組
(究竟是知道仍是不知道,我搞不清楚了。。。)那 LinkedHashMap 這篇也用了「你」和「好笑」,不知道到時候會不會有人繼續對號入座啊,想一想就以爲特別歡樂。緩存
在 HashMap 那篇文章裏,我有講解到一點,不知道同窗們記不記得,就是 null 會插入到 HashMap 的第一位。微信
Map<String, String> hashMap = new HashMap<>(); hashMap.put("沉", "沉默王二"); hashMap.put("默", "沉默王二"); hashMap.put("王", "沉默王二"); hashMap.put("二", "沉默王二"); hashMap.put(null, null); for (String key : hashMap.keySet()) { System.out.println(key + " : " + hashMap.get(key)); }
輸出的結果是:
null : null 默 : 沉默王二 沉 : 沉默王二 王 : 沉默王二 二 : 沉默王二
雖然 null 最後一位 put 進去的,但在遍歷輸出的時候,跑到了第一位。
那再來對比看一下 LinkedHashMap。
Map<String, String> linkedHashMap = new LinkedHashMap<>(); linkedHashMap.put("沉", "沉默王二"); linkedHashMap.put("默", "沉默王二"); linkedHashMap.put("王", "沉默王二"); linkedHashMap.put("二", "沉默王二"); linkedHashMap.put(null, null); for (String key : linkedHashMap.keySet()) { System.out.println(key + " : " + linkedHashMap.get(key)); }
輸出結果是:
沉 : 沉默王二 默 : 沉默王二 王 : 沉默王二 二 : 沉默王二 null : null
null 在最後一位插入,在最後一位輸出。
輸出結果能夠再次證實,HashMap 是無序的,LinkedHashMap 是能夠維持插入順序的。
那 LinkedHashMap 是如何作到這一點呢?我相信同窗們和我同樣,很是但願知道緣由。
要想搞清楚,就須要深刻研究一下 LinkedHashMap 的源碼。LinkedHashMap 並未重寫 HashMap 的 put()
方法,而是重寫了 put()
方法須要調用的內部方法 newNode()
。
HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, e); linkNodeLast(p); return p; }
前面說了,LinkedHashMap.Entry 繼承了 HashMap.Node,而且追加了兩個字段 before 和 after。
那,緊接着來看看 linkNodeLast()
方法:
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) { LinkedHashMap.Entry<K,V> last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
看到了吧,LinkedHashMap 在添加第一個元素的時候,會把 head 賦值爲第一個元素,等到第二個元素添加進來的時候,會把第二個元素的 before 賦值爲第一個元素,第一個元素的 afer 賦值爲第二個元素。
這就保證了鍵值對是按照插入順序排列的,明白了吧?
注:我用到的 JDK 版本爲 14。
LinkedHashMap 不只可以維持插入順序,還可以維持訪問順序。訪問包括調用 get()
方法、remove()
方法和 put()
方法。
要維護訪問順序,須要咱們在聲明 LinkedHashMap 的時候指定三個參數。
LinkedHashMap<String, String> map = new LinkedHashMap<>(16, .75f, true);
第一個參數和第二個參數,看過 HashMap 的同窗們應該很熟悉了,指的是初始容量和負載因子。
第三個參數若是爲 true 的話,就表示 LinkedHashMap 要維護訪問順序;不然,維護插入順序。默認是 false。
Map<String, String> linkedHashMap = new LinkedHashMap<>(16, .75f, true); linkedHashMap.put("沉", "沉默王二"); linkedHashMap.put("默", "沉默王二"); linkedHashMap.put("王", "沉默王二"); linkedHashMap.put("二", "沉默王二"); System.out.println(linkedHashMap); linkedHashMap.get("默"); System.out.println(linkedHashMap); linkedHashMap.get("王"); System.out.println(linkedHashMap);
輸出的結果以下所示:
{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二} {沉=沉默王二, 王=沉默王二, 二=沉默王二, 默=沉默王二} {沉=沉默王二, 二=沉默王二, 默=沉默王二, 王=沉默王二}
當咱們使用 get()
方法訪問鍵位「默」的元素後,輸出結果中,默=沉默王二
在最後;當咱們訪問鍵位「王」的元素後,輸出結果中,王=沉默王二
在最後,默=沉默王二
在倒數第二位。
也就是說,最不常常訪問的放在頭部,這就有意思了。有意思在哪呢?
咱們可使用 LinkedHashMap 來實現 LRU 緩存,LRU 是 Least Recently Used 的縮寫,即最近最少使用,是一種經常使用的頁面置換算法,選擇最近最久未使用的頁面予以淘汰。
public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> { private static final int MAX_ENTRIES = 5; public MyLinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor, accessOrder); } @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MAX_ENTRIES; } }
MyLinkedHashMap 是一個自定義類,它繼承了 LinkedHashMap,而且重寫了 removeEldestEntry()
方法——使 Map 最多可容納 5 個元素,超出後就淘汰。
咱們來測試一下。
MyLinkedHashMap<String,String> map = new MyLinkedHashMap<>(16,0.75f,true); map.put("沉", "沉默王二"); map.put("默", "沉默王二"); map.put("王", "沉默王二"); map.put("二", "沉默王二"); map.put("一枚有趣的程序員", "一枚有趣的程序員"); System.out.println(map); map.put("一枚有顏值的程序員", "一枚有顏值的程序員"); System.out.println(map); map.put("一枚有才華的程序員","一枚有才華的程序員"); System.out.println(map);
輸出結果以下所示:
{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員} {默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員, 一枚有顏值的程序員=一枚有顏值的程序員} {王=沉默王二, 二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員, 一枚有顏值的程序員=一枚有顏值的程序員, 一枚有才華的程序員=一枚有才華的程序員}
沉=沉默王二
和 默=沉默王二
依次被淘汰出局。
假如在 put 「一枚有才華的程序員」以前 get 了鍵位爲「默」的元素:
MyLinkedHashMap<String,String> map = new MyLinkedHashMap<>(16,0.75f,true); map.put("沉", "沉默王二"); map.put("默", "沉默王二"); map.put("王", "沉默王二"); map.put("二", "沉默王二"); map.put("一枚有趣的程序員", "一枚有趣的程序員"); System.out.println(map); map.put("一枚有顏值的程序員", "一枚有顏值的程序員"); System.out.println(map); map.get("默"); map.put("一枚有才華的程序員","一枚有才華的程序員"); System.out.println(map);
那輸出結果就變了,對吧?
{沉=沉默王二, 默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員} {默=沉默王二, 王=沉默王二, 二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員, 一枚有顏值的程序員=一枚有顏值的程序員} {二=沉默王二, 一枚有趣的程序員=一枚有趣的程序員, 一枚有顏值的程序員=一枚有顏值的程序員, 默=沉默王二, 一枚有才華的程序員=一枚有才華的程序員}
沉=沉默王二
和 王=沉默王二
被淘汰出局了。
那 LinkedHashMap 是如何來維持訪問順序呢?同窗們感興趣的話,能夠研究一下下面這三個方法。
void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { }
afterNodeAccess()
會在調用 get()
方法的時候被調用,afterNodeInsertion()
會在調用 put()
方法的時候被調用,afterNodeRemoval()
會在調用 remove()
方法的時候被調用。
我來以 afterNodeAccess()
爲例來說解一下。
void afterNodeAccess(HashMap.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; } }
哪一個元素被 get 就把哪一個元素放在最後。瞭解了吧?
那同窗們可能還想知道,爲何 LinkedHashMap 能實現 LRU 緩存,把最不常常訪問的那個元素淘汰?
在插入元素的時候,須要調用 put()
方法,該方法最後會調用 afterNodeInsertion()
方法,這個方法被 LinkedHashMap 重寫了。
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); } }
removeEldestEntry()
方法會判斷第一個元素是否超出了可容納的最大範圍,若是超出,那就會調用 removeNode()
方法對最不常常訪問的那個元素進行刪除。
因爲 LinkedHashMap 要維護雙向鏈表,因此 LinkedHashMap 在插入、刪除操做的時候,花費的時間要比 HashMap 多一些。
這也是沒辦法的事,對吧,欲戴皇冠必承其重嘛。既然想要維護元素的順序,總要付出點代價才行。
那這篇文章就到此戛然而止了,同窗們要以爲意猶未盡,請肆無忌憚地留言告訴我哦。(一不當心又在文末甩仨成語,有點文化底蘊,對吧?)
我是沉默王二,一枚有顏值卻僞裝靠才華苟且的程序員。關注便可提高學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,奧利給🌹。
注:若是文章有任何問題,歡迎絕不留情地指正。
若是你以爲文章對你有些幫助,歡迎微信搜索「沉默王二」第一時間閱讀;本文 GitHub github.com/itwanger 已收錄,歡迎 star。