面試官:如何用LinkedHashMap實現LRU

think123發表於2019-08-16

上一篇文章分析了HashMap的原理,有網友留言想看LinkedHashMap分析,今天它來了。

LinkedHashMap是HashMap的子類,在原有HashMap資料結構的基礎上,它還維護著一個雙向連結串列連結所有entry,這個連結串列定義了迭代順序,通常是資料插入的順序。

LinkedHashMap結構

上圖我只畫了連結串列,其實紅黑樹節點也是一樣的,只是節點型別不一樣而已

也就是說我們遍歷LinkedHashMap的時候,是從head指標指向的節點開始遍歷,一直到tail指向的節點。

原始碼

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
  static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }

    // 雙向連結串列頭節點
    transient LinkedHashMap.Entry<K,V> head;

    // 雙向連結串列尾節點
    transient LinkedHashMap.Entry<K,V> tail;

    // 指定遍歷LinkedHashMap的順序,true表示按照訪問順序,false表示按照插入順序,預設為false
    final boolean accessOrder;
  }

}
複製程式碼

從LinkedHashMap的定義裡面可以看到它單獨維護了一個雙向連結串列,用於記錄元素插入的順序。這也是為什麼我們列印LinkedHashMap的時候可以按照插入順序列印的支撐。而accessOrder屬性則指明瞭進行遍歷時是按照什麼順序進行訪問,我們可以通過它的構造方法自己指定順序。

public LinkedHashMap(int initialCapacity,float loadFactor,
                   boolean accessOrder) {
  super(initialCapacity, loadFactor);
  this.accessOrder = accessOrder;
}
複製程式碼

當accessOrder=true,訪問順序的輸出是什麼意思呢?來看下下面的一段程式碼

LinkedHashMap<Integer,Integer> map = new LinkedHashMap<>(8, 0.75f, true);

map.put(1, 1);
map.put(2, 2);
map.put(3, 3);

map.get(2);

System.out.println(map);

複製程式碼

輸出結果是

{1=1, 3=3, 2=2}
複製程式碼

可以看到get了的資料被放到了雙向連結串列尾部,也就是按照了訪問時間進行排序,這就是訪問順序的含義。

在插入的時候LinkedHashMap複寫了HashMap的newNode以及newTreeNode方法,並且在方法內部更新了雙向連結串列的指向關係。

同時插入的時候呼叫了afterNodeAccess()方法以及afterNodeInsertion()方法,在HashMap中這兩個方法是空實現,而在LinkedHashMap中則有具體實現,這兩個方法也是專門給LinkedHashMap進行回撥處理的。

afterNodeAccess()方法中如果accessOrder=true時會移動節點到雙向連結串列尾部。當我們在呼叫map.get()方法的時候如果accessOrder=true也會呼叫這個方法,這就是為什麼訪問順序輸出時訪問到的元素移動到連結串列尾部的原因。

接下來來看看afterNodeInsertion()的實現

// evict如果為false,則表處於建立模式,當我們new HashMap(Map map)的時候就處於建立模式
void afterNodeInsertion(boolean evict) { // possibly remove eldest
  LinkedHashMap.Entry<K,V> first;

  // removeEldestEntry 總是返回false,所以下面的程式碼不會執行。
  if (evict && (first = head) != null && removeEldestEntry(first)) {
      K key = first.key;
      removeNode(hash(key), key, null, false, true);
  }
}
複製程式碼

看到這裡我有一個想法,可以通過LinkedHashMap來實現LRU(Least Recently Used,即近期最少使用),只要滿足條件,就刪除head節點。

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

  /**
   * 判斷元素個數是否超過快取容量
   */
  @Override
  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
      return size() > cacheSize;
  }
}

複製程式碼

就這樣一個簡單的LRU Cache就實現了,以後面試官如果喊你給它實現一個LRU,你就這樣寫給他,如果他讓你換一種方式,你就用連結串列使用同樣的思維給他實現一個,然後你就可以收割offer了。

對於刪除,LinkedHashMap也同樣是在HashMap的刪除邏輯完成後,呼叫了afterNodeRemoval這個回撥方法來更正連結串列指向關係。

其實你只要看了上一篇文章再也不怕面試官問我JDK8 HashMap,再記得LinkedHashMap只是多維護了一個雙向連結串列之後,再看LinkedHashMap中關於連結串列操作的程式碼就非常簡單了。

相關文章