【Java面试经典】HashMap内部第一次初始化出来的数组多大,为什么这么设置

128 阅读5分钟

HashMap如果初始化设置12,内部第一次初始化出来的数组多大,为什么这么设置

  1. 初始化数组大小的计算规则

    • 在 Java 中,HashMap的容量(数组大小)总是 2 的幂次方。当你初始化HashMap并指定初始容量为n时,HashMap会计算出一个大于等于n的最小的 2 的幂次方作为实际的初始容量。
    • 对于给定的初始容量12HashMap会计算出大于等于12的最小 2 的幂次方,即16。这个计算过程是通过一系列位运算来实现的。在HashMap的源码中,tableSizeFor方法用于计算这个合适的容量大小,如下是相关代码片段:
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;
}
  • cap = 12为例,首先n = cap - 1 = 11(二进制为1011)。然后通过一系列无符号右移(>>>)和按位或(|)操作,目的是将n的二进制最高位的1以及其后面的所有位都变为1。经过这些操作后,n变为15(二进制为1111),最后n + 1得到16,这就是计算出的初始容量。

  1. 为什么要设置为 2 的幂次方

    • 提高哈希计算效率

      • HashMap通过(n - 1) & hash来计算元素在数组中的存储位置(n是数组长度,hash是键的哈希值)。当n为 2 的幂次方时,n - 1的二进制表示形式为低位全是1。这样在进行与运算(&)时,能够充分利用hash值的低位信息,使得元素能够更均匀地分布在数组中,减少哈希冲突。例如,如果n = 16(二进制为10000),n - 1 = 15(二进制为1111),在与hash值进行与运算时,能够根据hash值的后 4 位来确定存储位置,而hash值的后 4 位包含了丰富的信息,有助于均匀分布元素。
    • 便于位运算实现扩容操作

      • HashMap扩容时,新数组的长度也是 2 的幂次方。扩容后,元素在新数组中的位置可以通过简单的位运算来重新定位。假设原来的数组长度为n(2 的幂次方),扩容后的长度为2n,对于一个元素,其在原数组中的位置为i,在新数组中的位置可能是i或者i + n,通过简单的位运算(如判断hash值的某一位)就可以快速确定其在新数组中的位置,这种方式比其他复杂的计算方式更高效。
  2. 扩容后的位置计算原理

    • HashMap扩容时,容量变为原来的 2 倍(因为新容量也是 2 的幂次方)。假设原来的容量为(,为某个整数),那么扩容后的容量为。
    • 对于一个元素,它在原来数组中的存储位置是通过(n - 1) & hash计算得到的,设这个位置为。在扩容后,计算其在新数组中的位置可以使用类似的方式。由于新容量为,新的索引计算方式可以是((2n - 1) & hash)
    • 因为,所以的二进制表示就是在的二进制表示最高位前面添加一个1。例如,如果(二进制为10000),(二进制为1111),那么(二进制为100000),(二进制为11111)。
    • 当计算元素在新数组中的位置时,根据键的哈希值(hash)的最高位(在这个例子中,就是第位)来决定。如果hash的最高位是0,那么元素在新数组中的位置和在旧数组中的位置相同(即);如果hash的最高位是1,那么元素在新数组中的位置是。
  3. 位运算实现示例

    • 以一个简单的例子来说明。假设原来的容量(二进制为100),(二进制为11)。有一个键的哈希值hash = 7(二进制为111),通过(n - 1) & hash计算得到在原来数组中的位置(二进制计算:11 & 111 = 11)。
    • 当扩容后,新容量(二进制为1000),(二进制为111)。对于hash = 7,通过((2n - 1) & hash)计算得到在新数组中的位置。此时,hash的最高位(第 4 位)是0,所以元素在新数组中的位置仍然是(二进制计算:111 & 111 = 11)。
    • 再假设有一个键的哈希值hash = 13(二进制为1101)。在原来容量时,计算位置(二进制计算:11 & 1101 = 01)。扩容后,新容量,对于hash = 13hash的最高位(第 4 位)是1,所以在新数组中的位置是(二进制计算:111 & 1101 = 101)。
    • HashMap的源码中,通过这种位运算的方式高效地重新定位元素在扩容后的新数组中的位置,避免了复杂的重新哈希计算,从而提高了扩容操作的效率。这种基于 2 的幂次方容量和位运算的设计是HashMap性能优化的一个重要手段。