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

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

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

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

  ③、非線程安全mysql

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

  HashMap 集合能夠說是最重要的集合之一,上篇博客介紹的 HashSet 集合就是繼承 HashMap 來實現的。而本篇博客咱們介紹 Map 集合的另外一種實現——LinkedHashMap,其實也是繼承 HashMap 集合來實現的,並且咱們在介紹 HashMap 集合的 put 方法時,也指出了 put 方法中調用的部分方法在 HashMap 都是空實現,而在 LinkedHashMap 中進行了重寫。因此想要完全瞭解 LinkedHashMap 的實現原理,HashMap 的實現原理必定不能不懂。數組

一、LinkedHashMap 定義

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

  因此咱們能夠直接這樣說:LinkedHashMap = HashMap + LinkedList。LinkedHashMap 就是在 HashMap 的基礎上多維護了一個雙向鏈表,用來保證元素迭代順序。數據結構

  更形象化的圖形展現能夠直接移到文章末尾。mybatis

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

  

二、字段屬性

   ①、Entry<K,V>app

    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:關於雙向鏈表的介紹,能夠看這篇博客

三、構造函數

  ①、無參構造

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 的值。

四、添加元素

   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行以及5三、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) 方法,當元素的個數大於設定的最大個數,便移除首元素。

五、刪除元素

   同理也是調用 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     }

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

六、查找元素

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 小節 添加元素已經介紹過了,這就不在介紹。

七、遍歷元素

  在介紹 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         }

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

  打印結果:

  

八、迭代器

   咱們把上面遍歷的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 。

九、總結

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

  

 

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

相關文章
相關標籤/搜索