【问题标题】: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;

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

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

【问题讨论】:

    标签: java hashmap hashtable hash


    【解决方案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 的每一个版本变得越来越慢”的说法是相当具有误导性的。相反,位掩码操作以更快的速度变得更快,最终这两者都开始反映实际硬件的基本性能。在那个级别上,位掩码的性能肯定要高得多——足以让整个设置,包括额外的哈希码加扰步骤,仍然快得多。
    猜你喜欢
    • 2015-01-30
    • 2019-11-08
    • 2014-08-14
    • 2013-03-04
    • 2012-10-31
    相关资源
    最近更新 更多