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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM