当许多键具有相同的哈希码时,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 如何退化为平衡树?的主要内容,如果未能解决你的问题,请参考以下文章