HashMap之put方法流程解读

说明:本文中所谈论的HashMap基于JDK 1.8版本源码进行分析和说明。

HashMap的put方法算是HashMap中比较核心的功能了,复杂程度高但是算法巧妙,同时在上一版本的基础之上优化了存储结构,从链表逐步进化成了红黑树,以满足存取性能上的需要。本文逐行分析了put方法的执行流程,重点放在了对整个流程的把握,对于红黑树的执行逻辑只是点到为止,其实HashMap中还有很多细节算法值得分析和学习,本文没有涉及,算是抛砖引玉吧,后面抽空把其他的地方分析一番。

源码阅读与分析

1、HashMap的put方法,翻看源码:

 1 /**
 2  * Associates the specified value with the specified key in this map.
 3  * If the map previously contained a mapping for the key, the old
 4  * value is replaced.
 5  *
 6  * @param key key with which the specified value is to be associated
 7  * @param value value to be associated with the specified key
 8  * @return the previous value associated with <tt>key</tt>, or
 9  *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
10  *         (A <tt>null</tt> return can also indicate that the map
11  *         previously associated <tt>null</tt> with <tt>key</tt>.)
12  */
13 public V put(K key, V value) {
14     return putVal(hash(key), key, value, false, true);
15 }

2、紧接着调用内部方法putVal:

 1 /**
 2  * Implements Map.put and related methods
 3  *
 4  * @param hash hash for key
 5  * @param key the key
 6  * @param value the value to put
 7  * @param onlyIfAbsent if true, don't change existing value
 8  * @param evict if false, the table is in creation mode.
 9  * @return previous value, or null if none
10  */
11 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
12                boolean evict) {
13     Node<K,V>[] tab; Node<K,V> p; int n, i;
14     if ((tab = table) == null || (n = tab.length) == 0)
15         n = (tab = resize()).length;
16     if ((p = tab[i = (n - 1) & hash]) == null)
17         tab[i] = newNode(hash, key, value, null);
18     else {
19         Node<K,V> e; K k;
20         if (p.hash == hash &&
21             ((k = p.key) == key || (key != null && key.equals(k))))
22             e = p;
23         else if (p instanceof TreeNode)
24             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
25         else {
26             for (int binCount = 0; ; ++binCount) {
27                 if ((e = p.next) == null) {
28                     p.next = newNode(hash, key, value, null);
29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30                         treeifyBin(tab, hash);
31                     break;
32                 }
33                 if (e.hash == hash &&
34                     ((k = e.key) == key || (key != null && key.equals(k))))
35                     break;
36                 p = e;
37             }
38         }
39         if (e != null) { // existing mapping for key
40             V oldValue = e.value;
41             if (!onlyIfAbsent || oldValue == null)
42                 e.value = value;
43             afterNodeAccess(e);
44             return oldValue;
45         }
46     }
47     ++modCount;
48     if (++size > threshold)
49         resize();
50     afterNodeInsertion(evict);
51     return null;
52 }

拆解源码进行解析

1、put方法的注释

我们先把源码中的注释大致翻译一遍:

1 Associates the specified value with the specified key in this map.
2 If the map previously contained a mapping for the key, the old
3 value is replaced.
4 @param key key with which the specified value is to be associated
5 @param value value to be associated with the specified key
6 @return the previous value associated with <tt>key</tt>, or
7         <tt>null</tt> if there was no mapping for <tt>key</tt>.
8         (A <tt>null</tt> return can also indicate that the map
9         previously associated <tt>null</tt> with <tt>key</tt>.)

大意为:将指定的值与此映射中的指定键相关联,如果Map中已经包含了该键的映射,那么旧的映射值将会被替代,也就是说在put时,如果map中已经包含有key所关联的键值对,那么后续put进来的键值对,将会以相同key为准替换掉原来的那一对键值对。

返回的值则将是之前在map中实际与key相关联的Value值(也就是旧的值),如果key没有实际映射值的话那就返回null。

put方法作为对外暴露的方法,在内部实现时则立马调用了其内部putVal方法,并将put进去(覆盖)之前的结果k-v中的v进行了返回,但map中最新绑定的那一对k-v中的v已经是最新put的了。

1 /**
2 * 对外暴露的put方法
3 **/
4 public V put(K key, V value) {
5     return putVal(hash(key), key, value, false, true);
6 }

2、putVal方法中的第一个参数hash

我们先把源码中的方法上面的注释先浏览一遍:

 1 Computes key.hashCode() and spreads (XORs) higher bits of hash
 2 to lower.  Because the table uses power-of-two masking, sets of
 3 hashes that vary only in bits above the current mask will
 4 always collide. (Among known examples are sets of Float keys
 5 holding consecutive whole numbers in small tables.)  So we
 6 apply a transform that spreads the impact of higher bits
 7 downward. There is a tradeoff between speed, utility, and
 8 quality of bit-spreading. Because many common sets of hashes
 9 are already reasonably distributed (so don't benefit from
10 spreading), and because we use trees to handle large sets of
11 collisions in bins, we just XOR some shifted bits in the
12 cheapest possible way to reduce systematic lossage, as well as
13 to incorporate impact of the highest bits that would otherwise
14 never be used in index calculations because of table bounds.

大意为:将key的hashcode值(由native方法计算得到)再与该值的高16位进行异或运算得到最终的hash值。这样做的目的作者也给出了解释,就是通常的hash算法都总是碰撞,我们这样做的目的尽量使得hash值较为分散。(大概理解)

3、开始解析putVal里面的方法

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                boolean evict) {
 3     Node<K,V>[] tab; Node<K,V> p; int n, i;
 4     // 如果map还是空的,则先开始初始化,table是map中用于存放索引的表
 5     if ((tab = table) == null || (n = tab.length) == 0) {
 6         n = (tab = resize()).length;
 7     }
 8     // 使用hash与数组长度减一的值进行异或得到分散的数组下标,预示着按照计算现在的
 9     // key会存放到这个位置上,如果这个位置上没有值,那么直接新建k-v节点存放
10     // 其中长度n是一个2的幂次数
11     if ((p = tab[i = (n - 1) & hash]) == null) {
12         tab[i] = newNode(hash, key, value, null);
13     } 
14     // 如果走到else这一步,说明key索引到的数组位置上已经存在内容,即出现了碰撞
15     // 这个时候需要更为复杂处理碰撞的方式来处理,如链表和树
16     else {
17         Node<K,V> e; K k;
18         // 其中p已经在上面通过计算索引找到了,即发生碰撞那一个节点
19         // 比较,如果该节点的hash和当前的hash相等,而且key也相等或者
20         // 在key不等于null的情况下key的内容也相等,则说明两个key是
21         // 一样的,则将当前节点p用临时节点e保存
22         if (p.hash == hash &&
23             ((k = p.key) == key || (key != null && key.equals(k)))) {
24             e = p;
25         }
26         // 如果当前节点p是(红黑)树类型的节点,则需要特殊处理
27         // 如果是树,则说明碰撞已经开始用树来处理,后续的数据结构都是树而非
28         // 列表了
29         else if (p instanceof TreeNode) {
30             // 其中this表示当前HashMap, tab为map中的数组
31             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
32         }
33         else {
34             for (int binCount = 0; ; ++binCount) {
35                 // 如果当前碰撞到的节点没有后续节点,则直接新建节点并追加
36                 if ((e = p.next) == null) {
37                     p.next = newNode(hash, key, value, null);
38                     // TREEIFY_THRESHOLD = 8
39                     // 从0开始的,如果到了7则说明满8了,这个时候就需要转
40                     // 重新确定是否是扩容还是转用红黑树了
41                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
42                         treeifyBin(tab, hash);
43                     break;
44                 }
45                 // 找到了碰撞节点中,key完全相等的节点,则用新节点替换老节点
46                 if (e.hash == hash &&
47                     ((k = e.key) == key || (key != null && key.equals(k))))
48                     break;
49                 p = e;
50             }
51         }
52         // 此时的e是保存的被碰撞的那个节点,即老节点
53         if (e != null) { // existing mapping for key
54             V oldValue = e.value;
55             // onlyIfAbsent是方法的调用参数,表示是否替换已存在的值,
56             // 在默认的put方法中这个值是false,所以这里会用新值替换旧值
57             if (!onlyIfAbsent || oldValue == null)
58                 e.value = value;
59             // Callbacks to allow LinkedHashMap post-actions
60             afterNodeAccess(e);
61             return oldValue;
62         }
63     }
64     // map变更性操作计数器
65     // 比如map结构化的变更像内容增减或者rehash,这将直接导致外部map的并发
66     // 迭代引起fail-fast问题,该值就是比较的基础
67     ++modCount;
68     // size即map中包括k-v数量的多少
69     // 当map中的内容大小已经触及到扩容阈值时,则需要扩容了
70     if (++size > threshold)
71         resize();
72     // Callbacks to allow LinkedHashMap post-actions
73     afterNodeInsertion(evict);
74     return null;
75 }

4、当存储值发生碰撞,解决的方法已经转换为红黑树时,先看下红黑树的数据结构:

 1 /**
 2  * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
 3  * extends Node) so can be used as extension of either regular or
 4  * linked node.
 5  */
 6 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
 7     TreeNode<K,V> parent;  // red-black tree links
 8     TreeNode<K,V> left;
 9     TreeNode<K,V> right;
10     TreeNode<K,V> prev;    // needed to unlink next upon deletion
11     boolean red;
12 }

5、当存储值发生碰撞,并在当前节点已经延申到树时,将执行putTreeVal方法:

 1 /**
 2  * Tree version of putVal.
 3  */
 4 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
 5                                int h, K k, V v) {
 6     Class<?> kc = null;
 7     boolean searched = false;
 8     TreeNode<K,V> root = (parent != null) ? root() : this;
 9     for (TreeNode<K,V> p = root;;) {
10         int dir, ph; K pk;
11         if ((ph = p.hash) > h)
12             dir = -1;
13         else if (ph < h)
14             dir = 1;
15         else if ((pk = p.key) == k || (k != null && k.equals(pk)))
16             return p;
17         else if ((kc == null &&
18                   (kc = comparableClassFor(k)) == null) ||
19                  (dir = compareComparables(kc, k, pk)) == 0) {
20             if (!searched) {
21                 TreeNode<K,V> q, ch;
22                 searched = true;
23                 if (((ch = p.left) != null &&
24                      (q = ch.find(h, k, kc)) != null) ||
25                     ((ch = p.right) != null &&
26                      (q = ch.find(h, k, kc)) != null))
27                     return q;
28             }
29             dir = tieBreakOrder(k, pk);
30         }
31 
32         TreeNode<K,V> xp = p;
33         if ((p = (dir <= 0) ? p.left : p.right) == null) {
34             Node<K,V> xpn = xp.next;
35             TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
36             if (dir <= 0)
37                 xp.left = x;
38             else
39                 xp.right = x;
40             xp.next = x;
41             x.parent = x.prev = xp;
42             if (xpn != null)
43                 ((TreeNode<K,V>)xpn).prev = x;
44             moveRootToFront(tab, balanceInsertion(root, x));
45             return null;
46         }
47     }
48 }

这里面大概是一个红黑树的储值计算方法,需要有数据结构的理论知识加持,初看有点晦涩难懂。

6、在值发生碰撞并需要延续追加时,如果追加的链表长度大于8,那么需要重新评估当前是扩充数组还是将链表转换为红黑树来存储

 1 /**
 2  * Replaces all linked nodes in bin at index for given hash unless
 3  * table is too small, in which case resizes instead.
 4  */
 5 final void treeifyBin(Node<K,V>[] tab, int hash) {
 6     int n, index; Node<K,V> e;
 7     // MIN_TREEIFY_CAPACITY = 64
 8     // 如果当前map的数组为空,或者数组长度还小于64,则选择扩充数组长度
 9     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
10         // 扩充数组长度涉及到原内容的重新散列再存储
11         resize();
12     }
13     // 如果执行else if则说明数组长度已经大于64了,这个时候就使用了
14     // 红黑树来处理
15     else if ((e = tab[index = (n - 1) & hash]) != null) {
16         TreeNode<K,V> hd = null, tl = null;
17         do {
18             TreeNode<K,V> p = replacementTreeNode(e, null);
19             if (tl == null)
20                 hd = p;
21             else {
22                 p.prev = tl;
23                 tl.next = p;
24             }
25             tl = p;
26         } while ((e = e.next) != null);
27         if ((tab[index] = hd) != null)
28             // table表从此节点链接成树
29             hd.treeify(tab);
30     }
31 }

7、扩充数组长度方法resize,会将整个map中的k-v对重新散列存储,会消耗性能

  1 /**
  2  * Initializes or doubles table size.  If null, allocates in
  3  * accord with initial capacity target held in field threshold.
  4  * Otherwise, because we are using power-of-two expansion, the
  5  * elements from each bin must either stay at same index, or move
  6  * with a power of two offset in the new table.
  7  *
  8  * @return the table
  9  */
 10 final Node<K,V>[] resize() {
 11     Node<K,V>[] oldTab = table;
 12     int oldCap = (oldTab == null) ? 0 : oldTab.length;
 13     int oldThr = threshold;
 14     int newCap, newThr = 0;
 15     if (oldCap > 0) {
 16         // MAXIMUM_CAPACITY = 1 << 30 = 1073741824
 17         // Integer.MAX_VALUE = (1 << 31) - 1 = 2147483647
 18         // 如果已经到了最大容量了,那么就调整扩容的threshold阈值
 19         if (oldCap >= MAXIMUM_CAPACITY) {
 20             threshold = Integer.MAX_VALUE;
 21             return oldTab;
 22         }
 23         // DEFAULT_INITIAL_CAPACITY = 1 << 4
 24         // 否则的话,如果将目前的容量扩充2倍还在允许范围之内,则将容量
 25         // 扩充为原来的两倍,并且阈值也为原来的两倍
 26         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 27                  oldCap >= DEFAULT_INITIAL_CAPACITY)
 28             newThr = oldThr << 1; // double threshold
 29     }
 30     // 如果原始(或者初始)容量不大于0,且之前的阈值大于0,则将容量初始化为
 31     // 之前阈值的大小
 32     else if (oldThr > 0) // initial capacity was placed in threshold
 33         newCap = oldThr;
 34     else {               // zero initial threshold signifies using defaults
 35         // 执行这里的方法说明,初始参数中容量大小和阈值都不大于0,那么就用
 36         // map中的缺省值
 37         // DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16
 38         // DEFAULT_LOAD_FACTOR = 0.75f
 39         newCap = DEFAULT_INITIAL_CAPACITY;
 40         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 41     }
 42     // 如果新的阈值没有重新计算,那么先用加载因子计算出值
 43     // 如果新的容量大小和阈值大小都未超过限定值,则计算出的值可用,否则
 44     // 阈值就限定为容量真正允许的上限即Integer.MAX_VALUE
 45     if (newThr == 0) {
 46         float ft = (float)newCap * loadFactor;
 47         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 48                   (int)ft : Integer.MAX_VALUE);
 49     }
 50     threshold = newThr;
 51     @SuppressWarnings({"rawtypes","unchecked"})
 52         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 53     // table已经是扩容好的新table了
 54     // 老的table存在了oldTab中
 55     table = newTab;
 56     // 以下就是一个重新散列存储的过程了
 57     // 将老的tab中的node,按照key重新散列得到新得存储地址来存储,
 58     // 以此来完成扩充
 59     if (oldTab != null) {
 60         for (int j = 0; j < oldCap; ++j) {
 61             Node<K,V> e;
 62             if ((e = oldTab[j]) != null) {
 63                 oldTab[j] = null;
 64                 if (e.next == null)
 65                     newTab[e.hash & (newCap - 1)] = e;
 66                 else if (e instanceof TreeNode)
 67                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 68                 else { // preserve order
 69                     Node<K,V> loHead = null, loTail = null;
 70                     Node<K,V> hiHead = null, hiTail = null;
 71                     Node<K,V> next;
 72                     do {
 73                         next = e.next;
 74                         if ((e.hash & oldCap) == 0) {
 75                             if (loTail == null)
 76                                 loHead = e;
 77                             else
 78                                 loTail.next = e;
 79                             loTail = e;
 80                         }
 81                         else {
 82                             if (hiTail == null)
 83                                 hiHead = e;
 84                             else
 85                                 hiTail.next = e;
 86                             hiTail = e;
 87                         }
 88                     } while ((e = next) != null);
 89                     if (loTail != null) {
 90                         loTail.next = null;
 91                         newTab[j] = loHead;
 92                     }
 93                     if (hiTail != null) {
 94                         hiTail.next = null;
 95                         newTab[j + oldCap] = hiHead;
 96                     }
 97                 }
 98             }
 99         }
100     }
101     return newTab;
102 }

HashMap的put方法流程总结

1、put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作;

2、putVal方法中,先检查HashMap数据结构中的索引数组表是否位空,如果是的话则进行一次resize操作;

3、以HashMap索引数组表的长度减一与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置值为空,则新建一个k-v的新节点;

4、如果不满足的3的条件,则说明索引指定的数组位置的已经存在内容,这个时候称之碰撞出现

5、在上面判断流程走完之后,计算HashMap全局的modCount值,以便对外部并发的迭代操作提供修改的Fail-fast判断提供依据,于此同时增加map中的记录数,并判断记录数是否触及容量扩充的阈值,触及则进行一轮resize操作;

6、在步骤4中出现碰撞情况时,从步骤7开始展开新一轮逻辑判断和处理;

7、判断key索引到的节点(暂且称作被碰撞节点)的hash、key是否和当前待插入节点(新节点)的一致,如果是一致的话,则先保存记录下该节点;如果新旧节点的内容不一致时,则再看被碰撞节点是否是树(TreeNode)类型,如果是树类型的话,则按照树的操作去追加新节点内容;如果被碰撞节点不是树类型,则说明当前发生的碰撞在链表中(此时链表尚未转为红黑树),此时进入一轮循环处理逻辑中;

8、循环中,先判断被碰撞节点的后继节点是否为空,为空则将新节点作为后继节点,作为后继节点之后并判断当前链表长度是否超过最大允许链表长度8,如果大于的话,需要进行一轮是否转树的操作;如果在一开始后继节点不为空,则先判断后继节点是否与新节点相同,相同的话就记录并跳出循环;如果两个条件判断都满足则继续循环,直至进入某一个条件判断然后跳出循环;

9、步骤8中转树的操作treeifyBin,如果map的索引表为空或者当前索引表长度还小于64(最大转红黑树的索引数组表长度),那么进行resize操作就行了;否则,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该新节点;

10、最后,回到那个被记住的被碰撞节点,如果它不为空,默认情况下,新节点的值将会替换被碰撞节点的值,同时返回被碰撞节点的值(V)。

put<k, v>方法流程图

根据上面分析出的流程步骤,我大致画了一个put方法的流程图,以方便理解。

思考与优化

1、resize操作在当前索引表容量不足时发生,这个操作对put性能有一定的冲击(据说还会引起死循环),但是能够自行避免,如果在我们使用map的时候能够知道需要存入的记录数,则可以通过【 (记录数 / threshold) + 1 】的方式计算出一个map的初始容量,并在声明HashMap时将初始容量指定为这个计算值。多提一句,尽管我们按照这种方式计算出了一个能够最大包容我们预期k-v键值对的容量值,但是HashMap为了性能考虑,在我们初始化容量之后,其内部又使用了一个tableSizeFor的方法将这个值转换成了一个大于等于该值的最近的一个2次幂的数值,以方便后续其他的位操作,这个方法很巧妙,可以自行研究一下。但这个内部方法我们是不需要考虑和深究的,按照上面这个方法计算并初始化使用就行了。

posted @ 2019-05-22 15:34  Captain&D  阅读(19712)  评论(1编辑  收藏  举报