前言

环境

  • openjdk 14

    本文主要内容

  • 介绍HashMap实现映射的方式

  • 介绍HashMap解决Hash冲突的方式
  • 介绍HashMap rehash 的方式

    阅读建议

  • 已了解过散列表

    正文

    如何实现映射?

    源码

    在向HashMap 中添加新元素时,肯定是要进行映射的,因此可以写一个添加新元素的 Demo,然后通过调试来观察下映射的过程 ```java package cn.zjm404.stu.java.hashmap;

import java.util.HashMap;

public class HashTest { public static void main(String[] args) { HashMap map = new HashMap<>(); map.put(“key1”,”hello world”); } }

  1. 开始调试~<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601118583663-601448af-17ab-4116-b639-0810a79a3dd0.png#align=left&display=inline&height=186&margin=%5Bobject%20Object%5D&name=image.png&originHeight=372&originWidth=1920&size=65460&status=done&style=none&width=960)<br />调用put后会跳转到putVal方法中。这里还要对 key 进行一些处理,注意,这里的 hash 方法并不是映射函数,而是进行了扰动<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601118673743-50bd3dc3-92d2-4fa4-bad2-d7588c470e71.png#align=left&display=inline&height=163&margin=%5Bobject%20Object%5D&name=image.png&originHeight=326&originWidth=1920&size=84045&status=done&style=none&width=960)<br />以下为 hash 方法的内容,这里对key 的 hashcode 进行了一些处理,解决了 key 为 null时的特殊情况;让hashcode的高十六位与低十六位进行异或运算,既缩小了hashcode的范围减轻了之后映射位置时的计算压力,又使得低位有了高位的信息,让散列更松散,更均匀<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601118834183-7a080254-b4df-443a-a089-f9eb867e7645.png#align=left&display=inline&height=225&margin=%5Bobject%20Object%5D&name=image.png&originHeight=450&originWidth=1920&size=97427&status=done&style=none&width=960)<br />扰动后,进入putVal。这里红线圈起来的代码,是真正的映射方法,这里是用使用与运算来更快的完成取余操作,等价于 **hash值%桶长。**(注意这里的 n 必须为2的n次幂数才可以用 & 来实现取余,所以这也是桶长必须为2的n次幂数的原因之一。原理是,从二进制的角度,一个数除以2 相当于右移 n 位,因右移而消失的数就是余数,因此如果获取了被移走的那几位数,就获取了余数。举例来讲,7 / 2 就相当于 111 右移 1 位,为 11,转为十进制,3。获取被移走的数,就获得了余数,那么 111 & (2-1) 即是 111 & 1 = 1,余数为 1)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601119352657-cd095094-df4c-474b-a462-90fadce10b68.png#align=left&display=inline&height=307&margin=%5Bobject%20Object%5D&name=image.png&originHeight=614&originWidth=1920&size=164917&status=done&style=none&width=960)<br />此外,由于 HashMap 使用拉链法解决 Hash 冲突,如果槽位已被占用,也就是可能存在链的情况下,还需要通过比较 key 值来确定位置<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601121451652-ada6a861-98a5-4ed1-b3f5-afcbbf5d4ce5.png#align=left&display=inline&height=354&margin=%5Bobject%20Object%5D&name=image.png&originHeight=707&originWidth=1880&size=171532&status=done&style=none&width=940)<br />以上就是实现映射的相关源码
  2. <a name="WwLoj"></a>
  3. ### 总结
  4. 映射相关源码如下<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601121566274-ae1ac6b6-9201-4548-a1b6-7223f629017f.png#align=left&display=inline&height=106&margin=%5Bobject%20Object%5D&name=image.png&originHeight=212&originWidth=1920&size=42561&status=done&style=none&width=960)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601121643313-195adbef-66bd-4f3d-84b4-cf0521dde789.png#align=left&display=inline&height=51&margin=%5Bobject%20Object%5D&name=image.png&originHeight=102&originWidth=1920&size=26859&status=done&style=none&width=960)<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601121678430-376af1b1-8e73-478e-94a2-30892eeef8cd.png#align=left&display=inline&height=61&margin=%5Bobject%20Object%5D&name=image.png&originHeight=122&originWidth=1920&size=30756&status=done&style=none&width=960)<br />过程为:
  5. - 先对 hashcode 进行扰动,让 hashcode 的高十六位与低十六位进行异或运算,缩小 hashcode 范围,同时让低位拥有高位信息,使得散列更均匀
  6. - 使用 & 进行取余,通过 hashcode 桶长进行取余,获取元素在桶中的映射位置。如果该位置不为空,则桶中可能存在链,需要通过比较 key 值来确定在链中的位置
  7. <a name="cMShY"></a>
  8. ## 如何解决Hash冲突?
  9. <a name="B4v0W"></a>
  10. ### 源码
  11. 可以顺着之前的插入源码继续,上面已说明,HashMap 是使用拉链法来解决 hash 冲突,但是在链表中查询元素时,时间复杂度为 O(n),因此,如果链表过长的话,就会严重影响 HashMap 的查询性能,因此,当链表长度 **>=** 阈值(这里的默认阈值为 8)时,会将链表转为红黑树,优化使其在发生哈希冲突时,查询时间复杂度无限接近于 O(logn)。减1是因为要算上加入的这个元素<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601122587895-e8049652-b329-4f84-bef1-a4a594c07586.png#align=left&display=inline&height=422&margin=%5Bobject%20Object%5D&name=image.png&originHeight=843&originWidth=1920&size=198653&status=done&style=none&width=960)<br />当树的节点数 **<=** 阈值(默认阈值为 6)时,会取消树化,转为链表。这一步发生在 split 方法中,而 resize 方法调用了 split ,也就是在扩容的时候,会检测红黑树的节点是否小于阈值,小于则进行转换<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/1308518/1601122955423-71b70738-326b-49c6-a186-77b9cc06e0c0.png#align=left&display=inline&height=277&margin=%5Bobject%20Object%5D&name=image.png&originHeight=554&originWidth=1920&size=110204&status=done&style=none&width=960)
  12. <a name="0cVP5"></a>
  13. ### 总结
  14. - HashMap 使用拉链法解决 Hash 冲突
  15. - 当链表长度 >= 阈值(默认为 8)时,转为红黑树,而当红黑树的节点数 <= 阈值(默认为 6)时,转为链表
  16. <a name="zMxAt"></a>
  17. ## 扩容后,如何实现rehash?
  18. 当负载因子过大时(HashMap 的负载因子阈值为0.75,负载因子 = 所有节点数 / 桶长度),Hash冲突会剧增,为了减小 hash 冲突,就需要进行扩容(HashMap 是扩容为原来桶数的2倍),而当扩容后需要将旧桶中的值移动到新桶,这里就需要重新映射,也就是 rehashrehash 有两种方式,一种是渐进式,redis 就是使用的这种方式,一种是一次性 rehash ,也就是一次完成, HashMap 使用的就是这种方式,但是 一次性 rehash时,无法进行其它操作,如果旧值太多,就会影响使用。HashMap 是如何解决一次性 rehash 速度慢的问题呢?前面提到,桶的长度必须为 2 n 次幂数,而进行映射的方式是 (桶长度 -1 )& hash 因此,当扩容为原来两倍时,只有两种情况
  19. - 未改变,还是原来的映射位置
  20. - 改变,新位置为 旧位置下标*2
  21. ```java
  22. final Node<K,V>[] resize() {
  23. Node<K,V>[] oldTab = table;//保存旧节点
  24. int oldCap = (oldTab == null) ? 0 : oldTab.length;//记录旧桶数量
  25. int oldThr = threshold;//保存旧HashMap容量
  26. int newCap, newThr = 0;
  27. if (oldCap > 0) {
  28. if (oldCap >= MAXIMUM_CAPACITY) {//如果旧桶数量大于最大桶数量,则没办法扩容
  29. threshold = Integer.MAX_VALUE;
  30. return oldTab;
  31. }//新桶数量扩大为旧桶数量的两倍
  32. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  33. oldCap >= DEFAULT_INITIAL_CAPACITY)//如果小于最大桶数量且大于默认桶数量
  34. newThr = oldThr << 1; // double threshold 新可用桶数量扩容为旧可用桶数量的两倍
  35. }
  36. else if (oldThr > 0) // initial capacity was placed in threshold
  37. newCap = oldThr;//进行初始化
  38. else { // zero initial threshold signifies using defaults
  39. newCap = DEFAULT_INITIAL_CAPACITY;
  40. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  41. }
  42. if (newThr == 0) {
  43. float ft = (float)newCap * loadFactor;//实际可用的HashMap容量 = 容量 * 负载因子
  44. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  45. (int)ft : Integer.MAX_VALUE);//最大的实际可用容量为 Integer.MAX_VALUE
  46. }
  47. threshold = newThr;//更新容量为实际可用容量
  48. @SuppressWarnings({"rawtypes","unchecked"})
  49. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//设置桶数量为最大容量
  50. table = newTab;//更新为新 tab
  51. if (oldTab != null) {//如果旧HashMap中存在节点,进行转移
  52. for (int j = 0; j < oldCap; ++j) {
  53. Node<K,V> e;
  54. if ((e = oldTab[j]) != null) {
  55. oldTab[j] = null;
  56. if (e.next == null)//如果没有"链”,只用将桶中的数据移入到新HashMap中就可以了
  57. newTab[e.hash & (newCap - 1)] = e;
  58. else if (e instanceof TreeNode)//桶中存在“链”,为 TreeNode则交由红黑树处理
  59. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  60. else { // preserve order 为链表形式 扩容后,位置可能改变,因为是翻倍,也就是“加个1”,如果位置发生改变也就是原来位置*2。举例原来为11,扩容为111,原来hash值为1101,位置为1101&111 = 101,扩容后1101&1111新位置就是1101
  61. Node<K,V> loHead = null, loTail = null;//记录原位置不变
  62. Node<K,V> hiHead = null, hiTail = null;//记录原位置改变
  63. Node<K,V> next;
  64. do {
  65. next = e.next;//hash函数逻辑为 (oldCap-1)&e.hash。因此oldCap&e.hash==0则说明位置未改变
  66. if ((e.hash & oldCap) == 0) {//扩容后,hash值不变
  67. if (loTail == null)//链表头后无节点
  68. loHead = e;
  69. else//使用尾插法插入
  70. loTail.next = e;
  71. loTail = e;
  72. }
  73. else {//扩容后hash值改变,则新位置 = 原位置*2
  74. if (hiTail == null)
  75. hiHead = e;
  76. else
  77. hiTail.next = e;
  78. hiTail = e;
  79. }
  80. } while ((e = next) != null);
  81. if (loTail != null) {//扩容后,hash值未改变,那么就还是原来的位置
  82. loTail.next = null;
  83. newTab[j] = loHead;
  84. }
  85. if (hiTail != null) {//扩容后,hash值改变,则新位置为原位置*2
  86. hiTail.next = null;
  87. newTab[j + oldCap] = hiHead;
  88. }
  89. }
  90. }
  91. }
  92. }
  93. return newTab;
  94. }