JDK1.8源碼(九)——java.util.LinkedHashMap 類

  前面我們介紹了 Map 集合的一種典型實現  HashMap  ,關於 HashMap 的特性,我們再來複習一遍:

  ①、基於JDK1.8的HashMap是由數組+鏈表+紅黑樹組成,相對於早期版本的 JDK HashMap 實現,新增了紅黑樹作爲底層數據結構,在數據量較大且哈希碰撞較多時,能夠極大的增加檢索的效率。

  ②、允許 key 和 value 都爲 null。key 重複會被覆蓋,value 允許重複。

  ③、非線程安全

  ④、無序(遍歷HashMap得到元素的順序不是按照插入的順序)

  HashMap 集合可以說是最重要的集合之一,上篇博客介紹的 HashSet 集合就是繼承 HashMap 來實現的。而本篇博客我們介紹 Map 集合的另一種實現——LinkedHashMap,其實也是繼承 HashMap 集合來實現的,而且我們在介紹 HashMap 集合的 put 方法時,也指出了 put 方法中調用的部分方法在 HashMap 都是空實現,而在 LinkedHashMap 中進行了重寫。所以想要徹底瞭解 LinkedHashMap 的實現原理,HashMap 的實現原理一定不能不懂。

1、LinkedHashMap 定義

  LinkedHashMap 是基於 HashMap 實現的一種集合,具有 HashMap 集合上面所說的所有特點,除了 HashMap 無序的特點,LinkedHashMap 是有序的,因爲 LinkedHashMap 在 HashMap 的基礎上單獨維護了一個具有所有數據的雙向鏈表,該鏈表保證了元素迭代的順序。

  所以我們可以直接這樣說:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基礎上多維護了一個雙向鏈表,用來保證元素迭代順序。

  更形象化的圖形展示可以直接移到文章末尾。

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>

2、字段屬性

   ①、Entry<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);
        }
    }

  LinkedHashMap 的每個元素都是一個 Entry,我們看到對於 Entry 繼承自 HashMap 的 Node 結構,相對於 Node 結構,LinkedHashMap 多了 before 和 after 結構。

  下面是Map類集合基本元素的實現演變。

  LinkedHashMap 中 Entry 相對於 HashMap 多出的 before 和 after 便是用來維護 LinkedHashMap  插入 Entry 的先後順序的。

  ②、其它屬性

//用來指向雙向鏈表的頭節點
transient LinkedHashMap.Entry<K,V> head;
//用來指向雙向鏈表的尾節點
transient LinkedHashMap.Entry<K,V> tail;
//用來指定LinkedHashMap的迭代順序
//true 表示按照訪問順序,會把訪問過的元素放在鏈表後面,放置順序是訪問的順序
//false 表示按照插入順序遍歷
final boolean accessOrder;

   注意:這裏有五個屬性別搞混淆的,對於 Node  next 屬性,是用來維護整個集合中 Entry 的順序。對於 Entry before,Entry after ,以及 Entry head,Entry tail,這四個屬性都是用來維護保證集合順序的鏈表,其中前兩個before和after表示某個節點的上一個節點和下一個節點,這是一個雙向鏈表。後兩個屬性 head 和 tail 分別表示這個鏈表的頭節點和尾節點。

  PS:關於雙向鏈表的介紹,可以看這篇博客

3、構造函數

  ①、無參構造

1     public LinkedHashMap() {
2         super();
3         accessOrder = false;
4     }

  調用無參的 HashMap 構造函數,具有默認初始容量(16)和加載因子(0.75)。並且設定了 accessOrder = false,表示默認按照插入順序進行遍歷。

  ②、指定初始容量

1     public LinkedHashMap(int initialCapacity) {
2         super(initialCapacity);
3         accessOrder = false;
4     }

  ③、指定初始容量和加載因子

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

  ④、指定初始容量和加載因子,以及迭代規則

1     public LinkedHashMap(int initialCapacity,
2                          float loadFactor,
3                          boolean accessOrder) {
4         super(initialCapacity, loadFactor);
5         this.accessOrder = accessOrder;
6     }

  ⑤、構造包含指定集合中的元素

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

  上面所有的構造函數默認 accessOrder = false,除了第四個構造函數能夠指定 accessOrder 的值。

4、添加元素

   LinkedHashMap 中是沒有 put 方法的,直接調用父類 HashMap 的 put 方法。關於 HashMap 的put 方法,可以參看我對於 HashMap 的介紹

   我將方法介紹複製到下面:

 1     //hash(key)就是上面講的hash方法,對其進行了第一步和第二步處理
 2     public V put(K key, V value) {
 3         return putVal(hash(key), key, value, false, true);
 4     }
 5     /**
 6      * 
 7      * @param hash 索引的位置
 8      * @param key  鍵
 9      * @param value  值
10      * @param onlyIfAbsent true 表示不要更改現有值
11      * @param evict false表示table處於創建模式
12      * @return
13      */
14     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
15             boolean evict) {
16          Node<K,V>[] tab; Node<K,V> p; int n, i;
17          //如果table爲null或者長度爲0,則進行初始化
18          //resize()方法本來是用於擴容,由於初始化沒有實際分配空間,這裏用該方法進行空間分配,後面會詳細講解該方法
19          if ((tab = table) == null || (n = tab.length) == 0)
20              n = (tab = resize()).length;
21          //注意:這裏用到了前面講解獲得key的hash碼的第三步,取模運算,下面的if-else分別是 tab[i] 爲null和不爲null
22          if ((p = tab[i = (n - 1) & hash]) == null)
23              tab[i] = newNode(hash, key, value, null);//tab[i] 爲null,直接將新的key-value插入到計算的索引i位置
24          else {//tab[i] 不爲null,表示該位置已經有值了
25              Node<K,V> e; K k;
26              if (p.hash == hash &&
27                  ((k = p.key) == key || (key != null && key.equals(k))))
28                  e = p;//節點key已經有值了,直接用新值覆蓋
29              //該鏈是紅黑樹
30              else if (p instanceof TreeNode)
31                  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
32              //該鏈是鏈表
33              else {
34                  for (int binCount = 0; ; ++binCount) {
35                      if ((e = p.next) == null) {
36                          p.next = newNode(hash, key, value, null);
37                          //鏈表長度大於8,轉換成紅黑樹
38                          if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
39                              treeifyBin(tab, hash);
40                          break;
41                      }
42                      //key已經存在直接覆蓋value
43                      if (e.hash == hash &&
44                          ((k = e.key) == key || (key != null && key.equals(k))))
45                          break;
46                      p = e;
47                  }
48              }
49              if (e != null) { // existing mapping for key
50                  V oldValue = e.value;
51                  if (!onlyIfAbsent || oldValue == null)
52                      e.value = value;
53                  afterNodeAccess(e);
54                  return oldValue;
55              }
56          }
57          ++modCount;//用作修改和新增快速失敗
58          if (++size > threshold)//超過最大容量,進行擴容
59              resize();
60          afterNodeInsertion(evict);
61          return null;
62     }

   這裏主要介紹上面方法中,爲了保證 LinkedHashMap 的迭代順序,在添加元素時重寫了的4個方法,分別是第23行、31行以及53、60行代碼:

1 newNode(hash, key, value, null);
2 putTreeVal(this, tab, hash, key, value)//newTreeNode(h, k, v, xpn)
3 afterNodeAccess(e);
4 afterNodeInsertion(evict);

  ①、對於 newNode(hash,key,value,null) 方法

    HashMap.Node<K,V> newNode(int hash, K key, V value, HashMap.Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
                new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        //用臨時變量last記錄尾節點tail
        LinkedHashMap.Entry<K,V> last = tail;
        //將尾節點設爲當前插入的節點p
        tail = p;
        //如果原先尾節點爲null,表示當前鏈表爲空
        if (last == null)
            //頭結點也爲當前插入節點
            head = p;
        else {
            //原始鏈表不爲空,那麼將當前節點的上節點指向原始尾節點
            p.before = last;
            //原始尾節點的下一個節點指向當前插入節點
            last.after = p;
        }
    }

  也就是說將當前添加的元素設爲原始鏈表的尾節點。

  ②、對於 putTreeVal 方法

  是在添加紅黑樹節點時的操作,LinkedHashMap 也重寫了該方法的 newTreeNode 方法:

1     TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
2         TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
3         linkNodeLast(p);
4         return p;
5     }

  也就是說上面兩個方法都是在將新添加的元素放置到鏈表的尾端,並維護鏈表節點之間的關係。 

  ③、對於 afterNodeAccess(e) 方法,在 putVal 方法中,是當添加數據鍵值對的 key 存在時,會對 value 進行替換。然後調用 afterNodeAccess(e) 方法:

 1     //把當前節點放到雙向鏈表的尾部
 2     void afterNodeAccess(HashMap.Node<K,V> e) { // move node to last
 3         LinkedHashMap.Entry<K,V> last;
 4         //當 accessOrder = true 並且當前節點不等於尾節點tail。這裏將last節點賦值爲tail節點
 5         if (accessOrder && (last = tail) != e) {
 6             //記錄當前節點的上一個節點b和下一個節點a
 7             LinkedHashMap.Entry<K,V> p =
 8                     (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
 9             //釋放當前節點和後一個節點的關係
10             p.after = null;
11             //如果當前節點的前一個節點爲null
12             if (b == null)
13                 //頭節點=當前節點的下一個節點
14                 head = a;
15             else
16                 //否則b的後節點指向a
17                 b.after = a;
18             //如果a != null
19             if (a != null)
20                 //a的前一個節點指向b
21                 a.before = b;
22             else
23                 //b設爲尾節點
24                 last = b;
25             //如果尾節點爲null
26             if (last == null)
27                 //頭節點設爲p
28                 head = p;
29             else {
30                 //否則將p放到雙向鏈表的最後
31                 p.before = last;
32                 last.after = p;
33             }
34             //將尾節點設爲p
35             tail = p;
36             //LinkedHashMap對象操作次數+1,用於快速失敗校驗
37             ++modCount;
38         }
39     }

  該方法是在 accessOrder = true 並且 插入的當前節點不等於尾節點時,該方法纔會生效。並且該方法的作用是將插入的節點變爲尾節點,後面在get方法中也會調用。代碼實現可能有點繞,可以藉助下圖來理解:

   ④、在看 afterNodeInsertion(evict) 方法

1     void afterNodeInsertion(boolean evict) { // possibly remove eldest
2         LinkedHashMap.Entry<K,V> first;
3         if (evict && (first = head) != null && removeEldestEntry(first)) {
4             K key = first.key;
5             removeNode(hash(key), key, null, false, true);
6         }
7     }

  該方法用來移除最老的首節點,首先方法要能執行到if語句裏面,必須 evict = true,並且 頭節點不爲null,並且 removeEldestEntry(first) 返回true,這三個條件必須同時滿足,前面兩個好理解,我們看最後這個方法條件:

1     protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
2         return false;
3     }

  這就奇怪了,該方法直接返回的是 false,也就是說怎麼都不會進入到 if 方法體內了,那這是這麼回事呢?

  這其實是用來實現 LRU(Least Recently Used,最近最少使用)Cache 時,重寫的一個方法。比如在 mybatis-connector 包中,有這樣一個類:

 1 package com.mysql.jdbc.util;
 2 
 3 import java.util.LinkedHashMap;
 4 import java.util.Map.Entry;
 5 
 6 public class LRUCache<K, V> extends LinkedHashMap<K, V> {
 7     private static final long serialVersionUID = 1L;
 8     protected int maxElements;
 9 
10     public LRUCache(int maxSize) {
11         super(maxSize, 0.75F, true);
12         this.maxElements = maxSize;
13     }
14 
15     protected boolean removeEldestEntry(Entry<K, V> eldest) {
16         return this.size() > this.maxElements;
17     }
18 }

  可以看到,它重寫了 removeEldestEntry(Entry<K,V> eldest) 方法,當元素的個數大於設定的最大個數,便移除首元素。

5、刪除元素

   同理也是調用 HashMap 的remove 方法,這裏我不作過多的講解,着重看LinkedHashMap 重寫的第 46 行方法。

 1 public V remove(Object key) {
 2         Node<K,V> e;
 3         return (e = removeNode(hash(key), key, null, false, true)) == null ?
 4             null : e.value;
 5     }
 6     
 7     final Node<K,V> removeNode(int hash, Object key, Object value,
 8             boolean matchValue, boolean movable) {
 9         Node<K,V>[] tab; Node<K,V> p; int n, index;
10         //(n - 1) & hash找到桶的位置
11         if ((tab = table) != null && (n = tab.length) > 0 &&
12         (p = tab[index = (n - 1) & hash]) != null) {
13         Node<K,V> node = null, e; K k; V v;
14         //如果鍵的值與鏈表第一個節點相等,則將 node 指向該節點
15         if (p.hash == hash &&
16         ((k = p.key) == key || (key != null && key.equals(k))))
17         node = p;
18         //如果桶節點存在下一個節點
19         else if ((e = p.next) != null) {
20             //節點爲紅黑樹
21         if (p instanceof TreeNode)
22          node = ((TreeNode<K,V>)p).getTreeNode(hash, key);//找到需要刪除的紅黑樹節點
23         else {
24          do {//遍歷鏈表,找到待刪除的節點
25              if (e.hash == hash &&
26                  ((k = e.key) == key ||
27                   (key != null && key.equals(k)))) {
28                  node = e;
29                  break;
30              }
31              p = e;
32          } while ((e = e.next) != null);
33         }
34         }
35         //刪除節點,並進行調節紅黑樹平衡
36         if (node != null && (!matchValue || (v = node.value) == value ||
37                       (value != null && value.equals(v)))) {
38         if (node instanceof TreeNode)
39          ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
40         else if (node == p)
41          tab[index] = node.next;
42         else
43          p.next = node.next;
44         ++modCount;
45         --size;
46         afterNodeRemoval(node);
47         return node;
48         }
49         }
50         return null;
51     }

  我們看第 46 行代碼實現:

 1     void afterNodeRemoval(HashMap.Node<K,V> e) { // unlink
 2         LinkedHashMap.Entry<K,V> p =
 3                 (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
 4         p.before = p.after = null;
 5         if (b == null)
 6             head = a;
 7         else
 8             b.after = a;
 9         if (a == null)
10             tail = b;
11         else
12             a.before = b;
13     }

  該方法其實很好理解,就是當我們刪除某個節點時,爲了保證鏈表還是有序的,那麼必須維護其前後節點。而該方法的作用就是維護刪除節點的前後節點關係。

6、查找元素

1     public V get(Object key) {
2         Node<K,V> e;
3         if ((e = getNode(hash(key), key)) == null)
4             return null;
5         if (accessOrder)
6             afterNodeAccess(e);
7         return e.value;
8     }

  相比於 HashMap 的 get 方法,這裏多出了第 5,6行代碼,當 accessOrder = true 時,即表示按照最近訪問的迭代順序,會將訪問過的元素放在鏈表後面。

  對於 afterNodeAccess(e) 方法,在前面第 4 小節 添加元素已經介紹過了,這就不在介紹。

7、遍歷元素

  在介紹 HashMap 時,我們介紹了 4 中遍歷方式,同理,對於 LinkedHashMap 也有 4 種,這裏我們介紹效率較高的兩種遍歷方式:

  ①、得到 Entry 集合,然後遍歷 Entry

1         LinkedHashMap<String,String> map = new LinkedHashMap<>();
2         map.put("A","1");
3         map.put("B","2");
4         map.put("C","3");
5         map.get("B");
6         Set<Map.Entry<String,String>> entrySet = map.entrySet();
7         for(Map.Entry<String,String> entry : entrySet ){
8             System.out.println(entry.getKey()+"---"+entry.getValue());
9         }

  ②、迭代

1         Iterator<Map.Entry<String,String>> iterator = map.entrySet().iterator();
2         while(iterator.hasNext()){
3             Map.Entry<String,String> entry = iterator.next();
4             System.out.println(entry.getKey()+"----"+entry.getValue());
5         }

  這兩種效率都還不錯,通過迭代的方式可以對一邊遍歷一邊刪除元素,而第一種刪除元素會報錯。

  打印結果:

8、迭代器

   我們把上面遍歷的LinkedHashMap 構造函數改成下面的:

LinkedHashMap<String,String> map = new LinkedHashMap<>(16,0.75F,true);

  也就是說將 accessOrder = true,表示按照訪問順序來遍歷,注意看上面的 第 5 行代碼:map.get("B)。也就是說設置 accessOrder = true 之後,那麼 B---2 應該是最後輸出,我們看一下打印結果:

  結果跟預期一致。那麼在遍歷的過程中,LinkedHashMap 是如何進行的呢?

  我們追溯源碼:首先進入到 map.entrySet() 方法裏面:

  發現 entrySet = new LinkedEntrySet() ,接下來我們查看 LinkedEntrySet 類。

  這是一個內部類,我們查看其 iterator() 方法,發現又new 了一個新對象 LinkedEntryIterator,接着看這個類:

  這個類繼承 LinkedHashIterator。

 1     abstract class LinkedHashIterator {
 2         LinkedHashMap.Entry<K,V> next;
 3         LinkedHashMap.Entry<K,V> current;
 4         int expectedModCount;
 5 
 6         LinkedHashIterator() {
 7             next = head;
 8             expectedModCount = modCount;
 9             current = null;
10         }
11 
12         public final boolean hasNext() {
13             return next != null;
14         }
15 
16         final LinkedHashMap.Entry<K,V> nextNode() {
17             LinkedHashMap.Entry<K,V> e = next;
18             if (modCount != expectedModCount)
19                 throw new ConcurrentModificationException();
20             if (e == null)
21                 throw new NoSuchElementException();
22             current = e;
23             next = e.after;
24             return e;
25         }
26 
27         public final void remove() {
28             HashMap.Node<K,V> p = current;
29             if (p == null)
30                 throw new IllegalStateException();
31             if (modCount != expectedModCount)
32                 throw new ConcurrentModificationException();
33             current = null;
34             K key = p.key;
35             removeNode(hash(key), key, null, false, false);
36             expectedModCount = modCount;
37         }
38     }

  看到 nextNode() 方法,很顯然是通過遍歷鏈表的方式來遍歷整個 LinkedHashMap 。

9、總結

  通過上面的介紹,關於 LinkedHashMap ,我想直接用下面一幅圖來解釋:

  去掉紅色和藍色的虛線指針,其實就是一個HashMap。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章