HashMap 中的 resize() 方法详解

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

1. 前言

我们平常使用HashMap 的时候 ,应该使用比较多的就是 put 和 get 方法,好学的小伙伴,点进去 put 方法的源码,会发现在某些情况下 它会调用 resize()。此外,我们还可以发现在HashMap 中其他地方 调用这个 resize()方法的 也不少。那我们就来讨论下这个方法主要是干啥的。

官方翻译:初始化或加倍表的大小。如果为空,则按照字段阈值中持有的初始容量目标分配。否则,因为我们使用的是2的幂展开,所以每个容器中的元素必须保持在相同的索引,或者在新表中以2的偏移量幂移动。

2. 源码

源码还是比较长,我们还是一段一段的看 首先说一下,这些hashmap 自带的属性,和静态常量的含义我在前面 java HashMap 详解 -- (默认常量和构造函数) 中已经讲过了,不懂的可以去看看。

2.1 第一部分

图片.png

  • 定义一个变量 oldTab,保存原先 hashMap 中的 table。

  • 定义变量 oldCap,保存 oldTab 的长度(如果 oldTab == null ,则返回0)

  • 定义变量 oldThr,保存 原先的 threshold

  • 定义变量 newCap(保存 扩容后的table 的长度);newThr(扩容后的 threshold 的值),先把他们两初始化为0

  • 当 oldCap > 0 时

    • 如果 oldCap >= MAXIMUM_CAPACITY,则说明无法扩容了,则把 threshold置为 Integer.MAX_VALUE;并返回 oldTab;
    • 或者当,newCap = oldCap2 且 newCap < MAXIMUM_CAPACITY(最大容量) && oldCap >= 默认初始化容量 16; newThr = oldThr << 1(即 newThr = 2 oldThr),即扩容两倍
  • 或者当 oldThr > 0 ,说明它初始化容量时设置过阈值,则直接把 oldThr 赋给 newCap 即可。

  • 否则 则按默认的初始化规则,newCap = 16; newThr = 0.75*16 (进到这里则说明,原先的map,初始化时没有指定 initialCapactity 大小,所以没有给 threshold 赋值,即它为 0)

  • 当 newThr == 0时,则重新计算 newThr的值

    • 用 newCap * 负载因子,得到 float 类型的变量 ft,当(newCap和ft均小于 MAXIMUM_CAPACITY )时,把ft 赋给 newThr,否则把 Integer.MAX_VALUE 赋给 newThr。
  • 处理完后,把 newThr 赋给 threshold.

2.2 第二部分

2021-11-26_172600.jpg

  • 定义一个新表 newTable,它的长度就是扩容后的长度。
  • 使当前table 指向这个newTable
  • 当旧表不为null (这里主要考虑数据复制)
    • 遍历旧表,且当该元素不为null时 进入if判断
    • 当 e.next == null ,说明当前这个节点就只要它一个元素,我们只需要把它重新分配位置即可,(位置的具体计算方式,元素的hash值 & 扩容后的table长度-1 ),因为前面说过newCap 是2的幂数,所以newCap-1 的二进制形式是(1,11,111,1111 ...)这种形式的,与当前元素hash值去 &运算,最终结果就是 0---(newCap-1),刚好保证随机分配到hashMap的各个位置中去。
    • 或者,当该节点是一个 treeNode类型,则说明这个节点下的元素已经树华,会将树容器中的节点拆分为较低的树容器和较高的树容器,如果太小则取消树化,变成链表。具体可以看 java HashMap 详解 -- (默认常量和构造函数) ,后续这个方法我在细讲。
    • 否则的话说明这个节点下面是一个链表。
    • lohead : 低位首节点, loTail : 低位尾节点
    • hiHead : 高位首节点, hiTail : 高位尾节点 , next : 保存下一节点。
    • 遍历链表,当 e = next不为null,一直遍历
      • 首先用next 保存 e.next
      • 这里 使用的是 : e.hash & oldTab的长度 (前面说过 oldTable 的长度 也是2的幂数,所以这里的二进制形式是 0,10,100,1000,10000....)当它的结果为0 时,则说明它的hash值小于老数组的长度,即他再次分配还是会在原先老数组的位置上(即 低位数组)。所以如果低位尾结点为null ,就把它赋给 低位头结点,否则就赋给低位尾结点的next。最后使得 低位尾结点 = e。
      • 这里 当 e.hash & oldTab的长度 不为0,则说明这个元素分配的位置必在,新数组的高位上,所以当高位尾结点为null,直接把该元素赋给高位头结点,否则赋给高位尾结点的next,最后把高位尾结点元素置为 e。
    • 当低位尾结点不为null,则把 它的next 置为null,把它的头结点直接放到,新数组索引为j的位置上。
    • 当高位尾结点不为null,也把它的 next 置为 null,因为它是高位,索引把它放到 新数组索引 j+oldCap的位置上
  • 最后返回这个新数组即可。

猜你喜欢

转载自juejin.im/post/7035613144047157279