为啥 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 的幂?的主要内容,如果未能解决你的问题,请参考以下文章