为啥 HashMap 要求初始容量是 2 的幂?

Posted

技术标签:

【中文标题】为啥 HashMap 要求初始容量是 2 的幂?【英文标题】:Why does HashMap require that the initial capacity be a power of two?为什么 HashMap 要求初始容量是 2 的幂? 【发布时间】:2012-01-11 05:58:56 【问题描述】:

我在翻Java的HashMap源码的时候看到下面的

//The default initial capacity - MUST be a power of two.
static final int DEFAULT_INITIAL_CAPACITY = 16;

我的问题是为什么首先存在这个要求?我还看到允许创建具有自定义容量的 HashMap 的构造函数将其转换为 2 的幂:

int capacity = 1;
while (capacity < initialCapacity)
  capacity <<= 1;

为什么容量总是必须是二的幂?

另外,当执行自动重新散列时,究竟会发生什么?哈希函数也改变了吗?

【问题讨论】:

【参考方案1】:

映射必须计算出用于任何给定键的内部表索引,将任何int 值(可能为负)映射到[0, table.length) 范围内的值。当 table.length 是 2 的幂时,可以真的廉价地完成 - 并且在 indexFor 中:

static int indexFor(int h, int length) 
    return h & (length-1);

使用不同的表长度,您需要计算余数并确保它是非负数。这绝对是一个微优化,但可能是有效的:)

另外,当执行自动重新散列时,究竟会发生什么?哈希函数也改变了吗?

我不太清楚你的意思。使用了相同的哈希码(因为它们只是通过在每个键上调用 hashCode 来计算的),但是由于表长度的变化,它们在表中的分布不同。例如,当表长度为16时,5和21的哈希码最终都存储在表条目5中。当表长度增加到32时,它们将在不同的条目中。

【讨论】:

正是我想要的,谢谢。还有一个疑问,为什么 Entry 表是瞬态的,即使它保留了所有数据? @Sushant:表中的数据在 writeObject 中显式序列化(因此所有空条目都不会被写出)。使字段瞬态停止正常的序列化代码在对defaultWriteObject的调用中将其写出。 @JonSkeet h & (length-1) 如何处理底片?假设长度 =16 和 h = -7 @Jon 我正在尝试将您的答案与accepted answer here联系起来 这里不重要,但是Hashmap使用的key的hash不是key.hashCode()。散列是在key.hashCode() 之上应用的补充散列函数。这样做是为了防止糟糕的 hashCode 实现可能导致超出预期的冲突。【参考方案2】:

理想的情况实际上是使用素数大小作为HashMap 的后备数组。这样,您的密钥将更自然地分布在整个阵列中。但是,这适用于 mod 划分,并且随着 Java 的每个版本,该操作变得越来越慢。 从某种意义上说,2 的力量是你能想象到的最差的表大小,因为糟糕的哈希码实现更有可能在数组中产生键冲突。

因此,您会在 Java 的 HashMap 实现中找到另一个非常重要的方法,即 hash(int),它可以弥补糟糕的哈希码。

【讨论】:

是的,这很有意义,但作为额外的帮助,您能否多谈谈 hash(int) 函数如何改进原始哈希码。我看到它需要 xor 一些位,但我还没有完全理解它。 基本上,使用两个方法的力量使 hashCode 的低位成为重要的。对于糟糕的 hashCode 实现,这不会有太大差异(例如:10110111 和 00000111)。因此,随着所有位的移位,较高的位变得更重要。 “mod 操作随着 Java 的每一个版本变得越来越慢”的说法是相当具有误导性的。相反,位掩码操作以更快的速度变得更快,最终这两者都开始反映实际硬件的基本性能。在那个级别上,位掩码的性能肯定要高得多——足以让整个设置,包括额外的哈希码加扰步骤,仍然快得多。

以上是关于为啥 HashMap 要求初始容量是 2 的幂?的主要内容,如果未能解决你的问题,请参考以下文章

HashMap的初始长度为啥是16

为什么HashMap建议初始化容量,且容量为2的次幂?

关于HashMap容量的初始化,还有这么多学问。

HashMap之成员变量介绍

java中hashMap的默认大小为啥是2的幂

HashMap的初始容量和加载因子