搞懂 Java LinkedHashMap 源碼

LinkedHashMap 源碼分析

上週學習了 HashMap 的源碼感受收穫頗多,雖然紅黑樹這個坑本身尚未填,可是我沒臉沒皮的先看了 LinkedHashMap 的源碼。由於LinkedHashMap的確跟HashMap有很大關係,看完這篇文章相信你們也會有這種感受。因爲有了 HashMap 源碼的分析鋪墊,這篇文章咱們將從如下幾個方面來分析 LinkedHashMap的源碼:node

  1. LinkedHashMap 與 HashMap 的關係
  2. LinkedHashMap 雙向鏈表的構建過程
  3. LinkedHashMap 刪除節點的過程
  4. LinkedHashMap 如何維持訪問順序
  5. LinkedHashMap - LRU (Least Recently Used) 最簡單的構建方式

LinkedHashMap 與 HashMap 的關係

咱們先來看下 LinkedHashMap 的體系圖:面試

 

 

圖片很直接的說明了一個問題,那就是 LinkedHashMap 直接繼承自HashMap ,這也就說明了上文中咱們說到的 HashMap 一切重要的概念 LinkedHashMap 都是擁有的,這就包括了,hash 算法定位 hash 桶位置,哈希表由數組和單鏈表構成,而且當單鏈表長度超過 8 的時候轉化爲紅黑樹,擴容體系,這一切都跟 HashMap 同樣。那麼除了這麼多關鍵的相同點之外,LinkedHashMapHashMap 更增強大,這體如今:算法

  • LinkedHashMap 內部維護了一個雙向鏈表,解決了 HashMap 不能隨時保持遍歷順序和插入順序一致的問題
  • LinkedHashMap 元素的訪問順序也提供了相關支持,也就是咱們常說的 LRU(最近最少使用)原則。

接下介紹中也貫穿着這兩個不一樣點的源碼分析以及如何應用。segmentfault

LinkedHashMap 雙向鏈表的構建過程

爲了便於理解,在看具體源碼以前,咱們先看一張圖,這張圖能夠很好的體現 LinkedHashMap 中個各個元素關係:數組

 

 

假設圖片中紅黃箭頭表明元素添加順序,藍箭頭表明單鏈表各個元素的存儲順序。head 表示雙向鏈表頭部,tail 表明雙向鏈表尾部緩存

上篇文章分析的 HashMap 源碼的時候咱們有一張示意圖,說明了 HashMap 的存儲結構爲,數組 + 單鏈表 + 紅黑樹,從上邊的圖片咱們也能夠看出 LinkedHashMap 底層的存儲結構並無發生變化。ide

惟一變化的是使用雙向鏈表(圖中紅黃箭頭部分)記錄了元素的添加順序,咱們知道 HashMap 中的 Node 節點只有 next 指針,對於雙向鏈表而言只有 next 指針是不夠的,因此 LinkedHashMap 對於 Node 節點進行了拓展:源碼分析

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);
   }
}
複製代碼

LinkedHashMap 基本存儲單元 Entry<K,V> 繼承自 HashMap.Node<K,V>,並在此基礎上添加了 before 和 after 這兩個指針變量。這 before 變量在每次添加元素的時候將會連接上一次添加的元素,而上一次添加的元素的 after 變量將指向該次添加的元素,來造成雙向連接。值得注意的是 LinkedHashMap 並無覆寫任何關於 HashMap put 方法。因此調用 LinkedHashMap 的 put 方法實際上調用了父類 HashMap 的方法。爲了方便理解咱們這裏放一下 HashMap 的 putVal 方法。post

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
              boolean evict) {
              
   Node<K,V>[] tab; Node<K,V> p; int n, i;
   
   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);
   else {// 發生 hash 碰撞了
       Node<K,V> e; K k;
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           e = p;
       else if (p instanceof TreeNode){....}
       else {
          //hash 值計算出的數組索引相同,但 key 並不一樣的時候 循環整個單鏈表
           for (int binCount = 0; ; ++binCount) {
               if ((e = p.next) == null) {//遍歷到尾部
                    // 建立新的節點,拼接到鏈表尾部
                   p.next = newNode(hash, key, value, null);
                   ....
                   break;
               }
               //若是遍歷過程當中找到鏈表中有個節點的 key 與 當前要插入元素的 key 相同,
               //此時 e 所指的節點爲須要替換 Value 的節點,並結束循環
               if (e.hash == hash &&
                   ((k = e.key) == key || (key != null && key.equals(k))))
                   break;
               //移動指針    
               p = e;
           }
       }
       //若是循環完後 e!=null 表明須要替換e所指節點 Value
       if (e != null) {
           V oldValue = e.value//保存原來的 Value 做爲返回值
           // onlyIfAbsent 通常爲 false 因此替換原來的 Value
           if (!onlyIfAbsent || oldValue == null)
               e.value = value;
           afterNodeAccess(e);//該方法在 LinkedHashMap 中的實現稍後說明
           return oldValue;
       }
   }
   //操做數增長
   ++modCount;
   //若是 size 大於擴容閾值則表示須要擴容
   if (++size > threshold)
       resize();
   afterNodeInsertion(evict);
   return null;
}
複製代碼

能夠看出每次添加新節點的時候其實是調用 newNode 方法生成了一個新的節點,放到指定 hash 桶中,可是很明顯,HashMapnewNode 方法沒法完成上述所講的雙向鏈表節點的間的關係,因此 LinkedHashMap 複寫了該方法:學習

// HashMap newNode 中實現
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    return new Node<>(hash, key, value, next);
}

// LinkedHashMap newNode 的實現
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    // 將 Entry 接在雙向鏈表的尾部
    linkNodeLast(p);
    return p;
}
複製代碼

能夠看出雙向鏈表的操做必定在 linkNodeLast方法中實現:

/**
* 該引用始終指向雙向鏈表的頭部
*/
transient LinkedHashMap.Entry<K,V> head;

/**
* 該引用始終指向雙向鏈表的尾部
*/
transient LinkedHashMap.Entry<K,V> tail;

複製代碼
// newNode 中新節點,放到雙向鏈表的尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 添加元素以前雙向鏈表尾部節點
   LinkedHashMap.Entry<K,V> last = tail;
   // tail 指向新添加的節點
   tail = p;
   //若是以前 tail 指向 null 那麼集合爲空新添加的節點 head = tail = p
   if (last == null)
       head = p;
   else {
       // 不然將新節點的 before 引用指向以前當前鏈表尾部
       p.before = last;
       // 當前鏈表尾部節點的 after 指向新節點
       last.after = p;
   }
}
複製代碼

 

 

LinkedHashMap 鏈表建立步驟,可用上圖幾個步驟來描述,藍色部分是 HashMap 的方法,而橙色部分爲 LinkedHashMap 獨有的方法。

當咱們建立一個新節點以後,經過linkNodeLast方法,將新的節點與以前雙向鏈表的最後一個節點(tail)創建關係,在這部操做中咱們仍不知道這個節點究竟儲存在哈希表表的何處,可是不管他被放到什麼地方,節點之間的關係都會加入雙向鏈表。如上述圖中節點 3 和節點 4 那樣彼此擁有指向對方的引用,這麼作就能確保了雙向鏈表的元素之間的關係即爲添加元素的順序。

LinkedHashMap 刪除節點的操做

如插入操做同樣,LinkedHashMap 沒有重寫的 remove 方法,使用的仍然是 HashMap 中的代碼,咱們先來回憶一下 HashMap 中的 remove 方法:

public V remove(Object key) {
   Node<K,V> e;
   return (e = removeNode(hash(key), key, null, false, true)) == null ?
       null : e.value;
}

// HashMap 中實現
 final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
   Node<K,V>[] tab; Node<K,V> p; int n, index;
   //判斷哈希表是否爲空,長度是否大於0 對應的位置上是否有元素
   if ((tab = table) != null && (n = tab.length) > 0 &&
       (p = tab[index = (n - 1) & hash]) != null) {
       
       // node 用來存放要移除的節點, e 表示下個節點 k ,v 每一個節點的鍵值
       Node<K,V> node = null, e; K k; V v;
       //若是第一個節點就是咱們要找的直接賦值給 node
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           node = p;
       else if ((e = p.next) != null) {
            // 遍歷紅黑樹找到對應的節點
           if (p instanceof TreeNode)
               node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
           else {
                //遍歷對應的鏈表找到對應的節點
               do {
                   if (e.hash == hash &&
                       ((k = e.key) == key ||
                        (key != null && key.equals(k)))) {
                       node = e;
                       break;
                   }
                   p = e;
               } while ((e = e.next) != null);
           }
       }
       // 若是找到了節點
       // !matchValue 是否不刪除節點
       // (v = node.value) == value ||
                            (value != null && value.equals(v))) 節點值是否相同,
       if (node != null && (!matchValue || (v = node.value) == value ||
                            (value != null && value.equals(v)))) {
           //刪除節點                 
           if (node instanceof TreeNode)
               ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
           else if (node == p)
               tab[index] = node.next;
           else
               p.next = node.next;
           ++modCount;
           --size;
           afterNodeRemoval(node);// 注意這個方法 在 Hash表的刪除操做完成調用該方法
           return node;
       }
   }
   return null;
}
複製代碼

LinkedHashMap 經過調用父類的 HashMap 的 remove 方法將 Hash 表的中節點的刪除操做完成即:

  1. 獲取對應 key 的哈希值 hash(key),定位對應的哈希桶的位置
  2. 遍歷對應的哈希桶中的單鏈表或者紅黑樹找到對應 key 相同的節點,在最後刪除,並返回原來的節點。

對於 afterNodeRemoval(node) HashMap 中是空實現,而該方法,正是 LinkedHashMap 刪除對應節點在雙向鏈表中的關係的操做:

//  從雙向鏈表中刪除對應的節點 e 爲已經刪除的節點
void afterNodeRemoval(Node<K,V> e) { 
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 將 p 節點的先後指針引用置爲 null 便於內存釋放
    p.before = p.after = null;
    // p.before 爲 null,代表 p 是頭節點 
    if (b == null)
        head = a;
    else//不然將 p 的前驅節點鏈接到 p 的後驅節點
        b.after = a;
    // a 爲 null,代表 p 是尾節點
    if (a == null)
        tail = b;
    else //不然將 a 的前驅節點鏈接到 b 
        a.before = b;
}
複製代碼

所以 LinkedHashMap 節點刪除方式以下圖步驟同樣:

 

 

LinkedHashMap 維護節點訪問順序

上邊咱們分析了 LinkedHashMapHashMap 添加和刪除元素的不一樣,能夠看出除了維護 Hash表中元素的關係之外,LinkedHashMap 還在添加和刪除元素的時候維護着一個雙向鏈表。那麼這個雙向鏈表究竟有何用呢?咱們來看下邊這個例子,咱們對比一下在相同元素添加順序的時候,遍歷 Map 獲得的結果:

//Map<String, Integer> map = new HashMap<>();
    Map<String, Integer> map = new LinkedHashMap<>();
  // 使用三個參數的構造法方法來指定 accessOrder 參數的值
  //Map<String, Integer> map = new LinkedHashMap<>(10,0.75f,true);


   map.put("老大", 1);
   map.put("老二", 2);
   map.put("老三", 3);
   map.put("老四", 4);

   Set<Map.Entry<String, Integer>> entrySet = map.entrySet();
   Iterator iter1 = entrySet.iterator();
   

    while (iter1.hasNext()) {
      Map.Entry entry = (Map.Entry) iter1.next();
      System.out.print("key:  " + entry.getKey() + "   ");
      System.out.println("value:  " + entry.getValue());
    }
       
    System.out.println("老三的值爲:" + map.get("老三"));
    System.out.println("老大的值爲:" + map.put("老大",1000));
    
    Iterator iter2 = entrySet.iterator();
    while (iter2.hasNext()) {
      // 遍歷時,需先獲取entry,再分別獲取key、value
      Map.Entry entry = (Map.Entry) iter2.next();
      System.out.print("key:  " + entry.getKey() + "   ");
      System.out.println("value:  " + entry.getValue());
    }

複製代碼
/*** HashMap 遍歷結果*/
key:  老二   value:  2
key:  老四   value:  4
key:  老三   value:  3
key:  老大   value:  1
老三的值爲:3
老大的值爲:1
key:  老二   value:  2
key:  老四   value:  4
key:  老三   value:  3
key:  老大   value:  1000

/*** LinkedHashMap 遍歷結果*/
key:  老大   value:  1
key:  老二   value:  2
key:  老三   value:  3
key:  老四   value:  4
老三的值爲:3
老大的值爲:1
key:  老大   value:  1000
key:  老二   value:  2
key:  老三   value:  3
key:  老四   value:  4
複製代碼

由上述方法結果能夠看出:

  1. HashMap 的遍歷結果是跟添加順序並沒有關係
  2. LinkedHashMap 的遍歷結果就是添加順序

這就是雙向鏈表的做用。雙向鏈表能作的不只僅是這些,在介紹雙向鏈表維護訪問順序前咱們看來看一個重要的參數:

final boolean accessOrder;// 是否維護雙向鏈表中的元素訪問順序
複製代碼

該方法隨 LinkedHashMap 構造參數初始化,accessOrder 默認值爲 false,咱們能夠經過三個參數構造方法指定該參數的值,參數定義爲 final 說明外部不能改變。

public LinkedHashMap(int initialCapacity, float loadFactor) {
   super(initialCapacity, loadFactor);
   accessOrder = false;
}
 
public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
}

public LinkedHashMap() {
        super();
        accessOrder = false;
}

public LinkedHashMap(Map<? extends K, ? extends V> m) {
   super();
   accessOrder = false;
   putMapEntries(m, false);
}

//能夠指定 LinkedHashMap 雙向鏈表維護節點訪問順序的構造參數
public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}
   
複製代碼

咱們試着使用三個參數的構造方法來建立上述例子中的 Map,並查看結果以下

//第一次遍歷
key:  老大   value:  1
key:  老二   value:  2
key:  老三   value:  3
key:  老四   value:  4

老三的值爲:3
老大的值爲:1

//第二次遍歷
key:  老二   value:  2
key:  老四   value:  4
key:  老三   value:  3
key:  老大   value:  1000
複製代碼

能夠看出當咱們使用 access 爲 true 後,咱們訪問元素的順序將會在下次遍歷的時候體現,最後訪問的元素將最後得到。其實這一切在 HashMap 源碼中也早有伏筆, 還記得咱們在每次 putVal/get/repalce 最後都有一個 void afterNodeAccess(Node<K,V> e) 方法,該方法在 HashMap 中是空實現,可是在 LinkedHasMap 中該後置方法,將做爲維護節點訪問順序的重要方法,咱們來看下其實現:

//將被訪問節點移動到鏈表最後
void afterNodeAccess(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;
       //訪問節點的後驅置爲 null    
       p.after = null;
       //如訪問節點的前驅爲 null 則說明 p = head
       if (b == null)
           head = a;
       else
           b.after = a;
       //若是 p 不爲尾節點 那麼將 a 的前驅設置爲 b    
       if (a != null)
           a.before = b;
       else
           last = b;
           
       if (last == null)
           head = p;
       else {
           p.before = last;
           last.after = p;
       }
       tail = p;// 將 p 接在雙向鏈表的最後
       ++modCount;
   }
}
複製代碼

咱們如下圖舉例看下整個 afterNodeAccess 過程是是怎麼樣的,好比咱們該次操做訪問的是 13 這個節點,而 14 是其後驅,11 是其前驅,且 tail = 14 。在經過 get 訪問 13 節點後, 13變成了 tail 節點,而14變成了其前驅節點,相應的 14的前驅變成 11 ,11的後驅變成了14, 14的後驅變成了13.

 

 

由此咱們得知,LinkedHashMap 經過afterNodeAccess 這個後置操做,能夠在 accessOrde = true 的時候,使雙向鏈表維護哈希表中元素的訪問順序。

上述測試例子中是使用了 LinkedHashMap 的迭代器,因爲有雙向鏈表的存在,它相比 HashMap 遍歷節點的方式更爲高效,咱們來對比看下二者的迭代器中的 nextNode 方法:

// HashIterator nextNode 方法
 final Node<K,V> nextNode() {
       Node<K,V>[] t;
       Node<K,V> e = next;
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
       if (e == null)
           throw new NoSuchElementException();
        //遍歷 table 尋找下個存有元素的 hash桶   
       if ((next = (current = e).next) == null && (t = table) != null) {
           do {} while (index < t.length && (next = t[index++]) == null);
       }
       return e;
   }
   
  // LinkedHashIterator nextNode 方法
final LinkedHashMap.Entry<K,V> nextNode() {
       LinkedHashMap.Entry<K,V> e = next;
       if (modCount != expectedModCount)
           throw new ConcurrentModificationException();
       if (e == null)
           throw new NoSuchElementException();
       current = e;
       //直接指向了當前節點的 after 後驅節點
       next = e.after;
       return e;
   }

複製代碼

更爲明顯的咱們能夠查看二者的 containsValue 方法:

//LinkedHashMap 中 containsValue 的實現
public boolean containsValue(Object value) {
    // 直接遍歷雙向鏈表去尋找對應的節點
   for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
       V v = e.value;
       if (v == value || (value != null && value.equals(v)))
           return true;
   }
   return false;
}
//HashMap 中 containsValue 的實現
public boolean containsValue(Object value) {
   Node<K,V>[] tab; V v;
   if ((tab = table) != null && size > 0) {
        //遍歷 哈希桶索引
       for (int i = 0; i < tab.length; ++i) 
            //遍歷哈希桶中鏈表或者紅黑樹
           for (Node<K,V> e = tab[i]; e != null; e = e.next) {
               if ((v = e.value) == value ||
                   (value != null && value.equals(v)))
                   return true;
           }
       }
   }
   return false;
}
複製代碼

Java 中最簡單的 LRU 構建方式

LRU 是 Least Recently Used 的簡稱,即近期最少使用,相信作 Android 的同窗必定知道 LruCache 這個東西, Glide 的三級緩存中內存緩存中也使用了這個 LruCache 類。 有興趣的同窗能夠去查看一下Glide緩存源碼解析

LRU 算法實現的關鍵就像它名字同樣,當達到預約閾值的時候,這個閾值多是內存不足,或者容量達到最大,找到最近最少使用的存儲元素進行移除,保證新添加的元素可以保存到集合中。

下面咱們來說解下,Java 中 LRU 算法的最簡單的實現。咱們還記得在每次調用 HashMap 的 putVal 方法添加完元素後還有個後置操做,void afterNodeInsertion(boolean evict) { } 就是這個方法。 LinkedHashMap 重寫了此方法:

// HashMap 中 putVal 方法實現 evict 傳遞的 true,表示表處於建立模式。
public V put(K key, V value) {
   return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) { .... }


//evict 由上述說明大部分狀況下都傳 true 表示表處於建立模式
void afterNodeInsertion(boolean evict) { // possibly remove eldest
   LinkedHashMap.Entry<K,V> first;
   //因爲 evict = true 那麼當鏈表不爲空的時候 且 removeEldestEntry(first) 返回 true 的時候進入if 內部
   if (evict && (first = head) != null && removeEldestEntry(first)) {
       K key = first.key;
       removeNode(hash(key), key, null, false, true);//移除雙向鏈表中處於 head 的節點
   }
}

 //LinkedHashMap 默認返回 false 則不刪除節點。 返回 true 雙向鏈表中處於 head 的節點
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
   return false;
}
複製代碼

由上述源碼能夠看出,若是若是 removeEldestEntry(Map.Entry<K,V> eldest) 方法返回值爲 true 的時候,當咱們添加一個新的元素以後,afterNodeInsertion這個後置操做,將會刪除雙向鏈表最初的節點,也就是 head 節點。那麼咱們就能夠從 removeEldestEntry 方法入手來構建咱們的 LruCache 。

public class LruCache<K, V> extends LinkedHashMap<K, V> {

   private static final int MAX_NODE_NUM = 2<<4;

   private int limit;

   public LruCache() {
       this(MAX_NODE_NUM);
   }

   public LruCache(int limit) {
       super(limit, 0.75f, true);
       this.limit = limit;
   }

   public V putValue(K key, V val) {
       return put(key, val);
   }

   public V getValue(K key) {
       return get(key);
   }
   
   /**
    * 判斷存儲元素個數是否預約閾值
    * @return 超限返回 true,不然返回 false
    */
   @Override
   protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
       return size() > limit;
   }
}
複製代碼

咱們構建了一個 LruCache 類, 他繼承自 LinkedHashMap 在構建的時候,調用了 LinkedHashMap 的三個參數的構造方法且 accessOrder 傳入 true,並覆寫了 removeEldestEntry 方法,當 Map 中的節點個數超過咱們預約的閾值時候在 putValue 將會執行 afterNodeInsertion 刪除最近沒有訪問的元素。 下面咱們來測試一下:

//構建一個閾值爲 3 的 LruCache 類
    LruCache<String,Integer> lruCache = new LruCache<>(3);
    
    
    lruCache.putValue("老大", 1);
    lruCache.putValue("老二", 2);
    lruCache.putValue("老三", 3);
    
    lruCache.getValue("老大");
    
    //超過指定 閾值 3 再次添加元素的 將會刪除最近最少訪問的節點
    lruCache.putValue("老四", 4);
    
    System.out.println("lruCache = " + lruCache);
複製代碼

運行結果固然是刪除 key 爲 "老二" 的節點:

lruCache = {老三=3, 老大=1, 老四=4}

複製代碼

總結

本文並無從以往的增刪改查四種操做上去分析 LinkedHashMap 的源碼,而是經過 LinkedHashMap 中不一樣於 HashMap 的幾大特色來展開分析。

  1. LinkedHashMap 擁有與 HashMap 相同的底層哈希表結構,即數組 + 單鏈表 + 紅黑樹,也擁有相同的擴容機制。

  2. LinkedHashMap 相比 HashMap 的拉鍊式存儲結構,內部額外經過 Entry 維護了一個雙向鏈表。

  3. HashMap 元素的遍歷順序不必定與元素的插入順序相同,而 LinkedHashMap 則經過遍歷雙向鏈表來獲取元素,因此遍歷順序在必定條件下等於插入順序。

  4. LinkedHashMap 能夠經過構造參數 accessOrder 來指定雙向鏈表是否在元素被訪問後改變其在雙向鏈表中的位置。

看完這篇文章咱們也能夠輕鬆的回答面試題之 LinkedHashMap 與 HashMap 的區別了。這篇文章就到此結束了。意猶未盡的朋友能夠查看我以前的關於其餘集合源碼的分析。(皮了一下,我很開心~)

參考

相關文章
相關標籤/搜索