当许多键具有相同的哈希码时,Java 8 的 HashMap 如何退化为平衡树?

Posted

技术标签:

【中文标题】当许多键具有相同的哈希码时,Java 8 的 HashMap 如何退化为平衡树?【英文标题】:How does Java 8's HashMap degenerate to balanced trees when many keys have the same hash code? 【发布时间】:2015-07-21 17:54:12 【问题描述】:

我读到键应该实现Comparable 来定义排序。 HashMap 如何结合散列和自然排序来实现树?没有实现 Comparable 的类,或者当多个不可相互比较的 Comparable 实现是同一个映射中的键时,该怎么办?

【问题讨论】:

【参考方案1】:

HashMap 中的implementation notes comment 比我自己写的更能描述HashMap 的操作。了解树节点及其排序的相关部分是:

此映射通常用作分箱(分桶)哈希表,但是当箱变得太大时,它们会转换为 TreeNode 的箱,每个结构类似于 java.util.TreeMap 中的结构。 [...] TreeNode 的 bin 可以像任何其他 bin 一样被遍历和使用,但在填充过多时还支持更快的查找。 [...]

树箱(即元素都是 TreeNode 的箱)主要按 hashCode 排序,但在 tie 的情况下,如果两个元素属于相同的“C 类实现 Comparable”类型,则它们的 compareTo 方法用于排序. (我们保守地通过反射检查泛型类型来验证这一点——参见方法comparableClassFor)。当键具有不同的哈希值或可排序时,树箱增加的复杂性在提供最坏情况 O(log n) 操作时是值得的,因此,在 hashCode() 方法返回的值很差的意外或恶意使用下,性能会优雅地下降分布式的,以及许多键共享一个 hashCode 的,只要它们也是 Comparable 的。 (如果这些都不适用,与不采取预防措施相比,我们可能会浪费大约两倍的时间和空间。但唯一已知的案例源于糟糕的用户编程实践,这些实践已经很慢,以至于这没什么区别。)

当两个对象具有相同的哈希码但不能相互比较时,调用方法 tieBreakOrder 打破平局,首先通过字符串比较 getClass().getName() (!),然后比较 System.identityHashCode

实际的树构建从 treeifyBin 开始,从 bin 到达 TREEIFY_THRESHOLD(当前为 8)开始,假设哈希表至少具有 MIN_TREEIFY_CAPACITY 容量(当前为 64)。这是一个基本正常的红黑树实现 (crediting CLR),但支持以与哈希箱相同的方式遍历的一些复杂性(例如,removeTreeNode)。

【讨论】:

但是如果 hashcode() 没有在对象中被覆盖,那么它无论如何都会调用 System.identityHashCode() 来查找 hashbucket。那么如果 key 没有覆盖 hashcode() 并且调用 tieBreakOrder 会发生什么?或者它会被调用吗? @RohitSachan 如果密钥类型使用默认身份语义,则任何存储桶都不太可能变得足够满以转换为树存储桶,因为身份哈希码分布良好。但是这种情况没有特殊处理,所以是的,tieBreakOrder 仍然会调用identityHashCode 进行订购。 @Rohit Sachan:如果每次优化尝试都失败,它必须求助于 Java 8 之前的版本中已知的相同链表行为。 @NateGlenn find 在不可比较的情况下(最后一个 else-if 和 else)似乎是双向的,但我需要在调试器中逐步确认。但是,这引发了一个问题,为什么在这种情况下需要命令。 @NateGlenn 推测:字符串是最常见的 JDK 控制的类型。每个额外的类检查在特殊情况下带来的好处会减少,而在一般情况下会增加成本,也许只有 String 是最佳权衡。请注意,String 是最终的,因此它不是用于子类防御。可能是为了防止基于哈希冲突的DoS攻击; String hashCode 并不总是最好的。在某些时候,有一大串散列为 MIN_VALUE 的字符串,包括“polygenelubricants”。我想其他值的碰撞也是可能的。【参考方案2】:

阅读code。主要是red-black tree。

它实际上不需要实现Comparable,但可以使用它(例如参见find method)

【讨论】:

【参考方案3】:

HashMap 有它自己的哈希方法,它对里面的对象应用一个补充的 2 位长度哈希以避免这个问题:

对给定的 hashCode 应用补充散列函数,以防止劣质散列函数。这很关键,因为 HashMap 使用长度为 2 的幂的哈希表,否则会遇到低位没有差异的 hashCode 的冲突。注意:空键总是映射到哈希 0,因此索引为 0。

如果您想了解它是如何完成的,请查看source of the HashMap class 内部。

static int hash(int h) 
    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);

【讨论】:

其实,没有。 hash 函数减少但不消除冲突,因此HashMap 仍然必须管理具有相同哈希值的键。 您已链接到旧版本的源代码。查看 Java 8 代码,您会发现它不再进行补充重新散列。该方法已不存在。

以上是关于当许多键具有相同的哈希码时,Java 8 的 HashMap 如何退化为平衡树?的主要内容,如果未能解决你的问题,请参考以下文章

对象哈希码

python 中关于字典的键

如果两个不同的对象具有相同的哈希码会发生啥?

创建具有不同键的哈希数组的 CSV

ruby 在哈希中删除相同的值多个键

Cassandra 的哈希值是不是跨多个表具有相同的值?