为什么hashMap的容量扩容时一定是2的幂次

👌为什么hashMap的容量扩容时一定是2的幂次?

题目详细答案

在HashMap中,初始化设置长度时,容量自动转成 2 的幂次长度,这样设计有几个重要原因,主要是为了优化性能和简化计算。

高效计算索引

HashMap使用哈希值来确定键值对在哈希表中的位置。为了计算数组索引,HashMap使用按位与操作代替取模运算。具体来说,HashMap通过以下方式计算索引:

1
int index= (n - 1) & hash;

其中n是哈希表数组的长度。假设n是 2 的幂次,比如 16(2^4),则n - 1是 15(1111 二进制)。这样,(n - 1) & hash操作可以快速地对哈希值进行取模运算,而不需要使用性能较低的取模操作%。

例如,如果n是 16(2^4):

  • n - 1是 15(1111 二进制)
  • 按位与操作(n - 1) & hash只保留哈希值的低 4 位,这相当于对 16 取模。

这种方式不仅计算快速,而且代码简洁。

减少哈希冲突

在哈希表中,哈希冲突是一个主要问题。哈希冲突发生时,不同的键计算出的索引相同,导致它们被存储在同一个桶中。通过将容量设置为 2 的幂次,哈希表能够更均匀地分布哈希值,减少冲突。

具体来说,当容量是 2 的幂次时,哈希值的低位和高位都能均匀地影响最终索引。这是因为扰动函数hash = h ^ (h >>> 16)将高位和低位混合在一起,使得哈希值的分布更均匀。

假设我们有一个HashMap,其容量为 10(不是 2 的幂次),并且我们有一组键,它们的哈希值分别为:

  • 键 A 的哈希值:35
  • 键 B 的哈希值:45
  • 键 C 的哈希值:55

(1)使用非 2 的幂次容量

如果容量为 10,我们计算索引的方法是取模运算:

  • 键 A 的索引:35 % 10 = 5
  • 键 B 的索引:45 % 10 = 5
  • 键 C 的索引:55 % 10 = 5

可以看到,这些不同的键在取模运算后都映射到同一个索引 5,导致了哈希冲突。

(2)使用 2 的幂次容量

现在假设我们将容量设置为 16(2^4),并使用按位与操作来计算索引:

  • 键 A 的索引:35 & (16 - 1) = 35 & 15 = 3
  • 键 B 的索引:45 & (16 - 1) = 45 & 15 = 13
  • 键 C 的索引:55 & (16 - 1) = 55 & 15 = 7

在这种情况下,这些键映射到不同的索引(3、13 和 7),没有发生哈希冲突。

简化扩容

HashMap在需要扩容时,通常会将容量加倍。如果容量总是 2 的幂次,那么加倍后的容量仍然是 2 的幂次,这样可以简化扩容过程中的计算和重新哈希操作。

内存对齐和效率

计算机内存分配通常更高效地处理 2 的幂次大小的内存块。使用 2 的幂次长度可以更好地利用内存对齐,提高内存访问效率。

实现细节

当你初始化HashMap时,指定的初始容量会被调整为大于或等于该值的最小的 2 的幂次。例如,如果你指定的初始容量是 10,HashMap会将其调整为 16(2^4)。

具体实现如下:

1
2
3
4
5
6
7
8
9
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法通过一系列的位移和按位或操作,将任意整数调整为大于或等于它的最小 2 的幂次。但是还有一种特殊情况套用以上公式不行,这些数字就是2的幂自身。如果数字4 套用公式的话。得到的会是 8 ,为了解决这个问题,JDK的工程师把所有用户传进来的数在进行计算之前先-1。

/yfxseowdd8g5hm0e>

 wechat
天生我才必有用