为什么HashMap会重新计算键对象提供的哈希码?

16

我正在阅读Java 1.6 API提供的HashMap类代码,但无法完全理解以下操作的必要性(在put和get方法的主体中找到):

int hash = hash(key.hashCode());

hash() 方法的实现如下:

 private static int hash(int h) {
         h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

这实际上通过在提供的哈希码上执行位运算来重新计算哈希。尽管API如下所述,但我无法理解为什么需要这样做:

这很关键,因为HashMap使用长度为2的幂的哈希表,否则会遇到在较低位中没有差异的哈希码之间发生冲突的情况。

我确实理解键值对存储在数据结构数组中,并且该数组中的项目的索引位置由其哈希确定。但我不明白这个函数如何为哈希分布增添任何价值。

4个回答

25

正如Helper所述,这只是为了防止现有的键对象哈希函数存在问题并且不能很好地混合低位。根据pgras引用的源代码

 /**
  * Returns index for hash code h.
  */
 static int indexFor(int h, int length) {
     return h & (length-1);
 }
哈希值被与2的幂长度相与(因此,保证length-1是一串1)。由于这种与操作,只有h的低位被使用,其余部分被忽略。想象一下,由于某种原因,原始哈希函数只返回可以被2整除的数字。如果直接使用它,则哈希映射表中的奇数位置将永远不会被使用,导致冲突数量增加了2倍。在极端情况下,糟糕的哈希函数可能会使哈希映射表行为更像是一个列表,而不是O(1)容器。
Sun工程师必须已经运行了测试,显示许多哈希函数的低位不够随机,并且许多哈希映射表永远无法使用高位。在这些情况下,HashMap的hash(int h)中的位运算可以提供比大多数预期用例更好的性能改进(由于更低的冲突率),尽管需要额外的计算。

3
“just in case”? 实际上,Java中大多数散列码都可能很差。例如,看看java.lang.Integer就知道了!但这其实是有道理的。只要遵循“相等的对象具有相等的哈希码”的规则,并尽可能避免碰撞,每个人的Object.hashCode()都有着低效的位分布也没问题。然后只有像HashMap这样的集合实现需要通过二次哈希函数传递这些值,而不是所有人都需要处理这个问题。 - Kevin Bourrillion
哈希映射的奇数位置永远不会被使用。我不明白它的意思。你能给个例子吗? - Dean Chen
2
好的,假设我正在对雇员对象进行哈希处理,而我所有的雇员都有一个 int ID 字段,例如“400114”、“400214”、“400314”等(它们都共享我的部门后缀“14”部分)。 Integer 的 hashCode() 方法返回整数本身 - 因此,如果我将员工 ID 用作 HashSet/没有/ HashMap 的 hash(int h) 的键,则扩展将非常不平衡。在这个例子中,由于14是偶数,因此只有偶数桶会被使用。 - tucuxi
@tucuxi,那我可以把hash(int h)看作是用于均匀分布的二级哈希吗? - roottraveller

2

我曾在某处读到,这样做是为了确保好的分布,即使您的hashCode实现有点“烂”。


没错,在Java中,java.lang.Object类中的默认hashcode()实现在哈希之间没有太大的分布。 - Sam Barnum
我不明白的是,如果每个哈希值都是唯一的(并且所讨论的方法不能解决唯一哈希的问题),那么这种机制会遇到什么问题?文中提到了低位比特的碰撞问题,但并不是很清楚。 - VGDIV
每个哈希值本质上都不是唯一的...我无法对你的问题给出一个好的答案,但问题在于“indexFor”方法返回“hashCode & (length-1)”... - pgras

2

正如您所知,使用哈希映射时,其底层实现是哈希表,具体来说是闭合桶哈希表。负载因子决定了集合中对象的适当数量/桶的总数。

假设您不断添加更多元素。每次这样做,如果不是更新操作,则运行对象的哈希码方法,并使用模运算符来确定对象应该放入哪个桶中的桶的数量。

随着n(集合中元素的数量)/ m(桶的数量)变得越来越大,读写性能会越来越差。

即使您的哈希码算法很棒,性能仍然取决于此比较n/m。

重新散列也用于更改桶的数量,并保持构建集合时的相同负载因子。

请记住,任何哈希实现的主要优点是理想的O(1)读写性能。


1

正如你所知,object.hashCode()可以被用户重写,因此一个非常糟糕的实现会产生非随机的低位比特。这将倾向于拥挤一些桶,并留下许多未填充的桶。

我刚刚创建了一个视觉地图,展示了他们在哈希中试图做什么。似乎hash(int h)方法只是通过位级操作创建一个随机数,以便生成的数字更随机(因此更均匀地分布到桶中)。

每个位都被重新映射到不同的位上,具体如下:

        h1 = h1 ^ h13 ^ h21 ^ h9 ^ h6     
        h2 = h2 ^ h14 ^ h22 ^ h10 ^ h7
        h3 = h3 ^ h15 ^ h23 ^ h11 ^ h8
        h4 = h4 ^ h16 ^ h24 ^ h12 ^ h9
        h5 = h5 ^ h17 ^ h25 ^ h13 ^ h10

. . . .

直到h12。

正如您所看到的,每个h的位都将相距很远。因此,它几乎是随机的,不会聚集在任何特定的桶中。希望这可以帮助您。如果您需要完整的可视化,请给我发送电子邮件。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接