1. 程式人生 > >JDK1.8原始碼(九)——java.util.LinkedHashMap 類

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     }
View Code

   這裡主要介紹上面方法中,為了保證 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     }
View Code

  我們看第 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。