当许多键具有相同的哈希码时,Java 8的HashMap如何退化为平衡树?我读到说键应该实现Comparable
来定义排序。 HashMap如何结合哈希和自然排序来实现树?对于不实现Comparable
的类,或者当多个非相互可比较的Comparable
实现是映射中的键时怎么办?
HashMap的实现说明注释比我自己写的更好地描述了HashMap的操作。了解树节点及其顺序的相关部分如下:
该映射通常作为一个箱式散列表,但当箱子太大时,它们会转换成TreeNodes的箱子,每个TreeNodes的结构与java.util.TreeMap中的类似。[...] TreeNodes的箱子可以像其他箱子一样遍历和使用,但在过度填充时还支持更快的查找。 [...]
树形箱(即元素全部为TreeNodes的箱)首先按hashCode排序,但在出现冲突的情况下,如果两个元素属于相同的“C implements Comparable”类型,则使用它们的compareTo方法进行排序。(我们通过反射谨慎地检查泛型类型以验证这一点--请参见方法comparableClassFor)。树箱的增加复杂性值得提供最坏情况下O(log n)操作,当键具有不同的哈希或可排序时。因此,在hashCode()方法返回分布较差的值或许多键共享hashCode的情况下,性能会逐渐降低,只要它们也是可比较的。(如果这两种情况都不适用,则与不采取预防措施相比,我们可能会浪费大约两倍的时间和空间。但已知的唯一情况是由于糟糕的用户编程实践导致的,其已经如此缓慢,以至于这几乎没有任何区别。)
当两个对象具有相等的哈希码但不可相互比较时,将调用方法tieBreakOrder
来打破平局。首先通过对getClass().getName()
进行字符串比较,然后通过比较System.identityHashCode
来解决。
实际的树构建始于treeifyBin
,当桶达到TREEIFY_THRESHOLD
(目前为8)时开始,假设哈希表的容量至少为MIN_TREEIFY_CAPACITY
(目前为64)。这是一个基本上正常的红黑树实现(向CLR致谢),但有一些复杂的部分来支持以与哈希桶相同的方式进行遍历(例如,removeTreeNode
)。
HashMap
有它自己的哈希方法,它对内部对象应用了一个补充的 2 位长度哈希以避免出现问题:
对给定的哈希码应用补充哈希函数,这可以防止低质量哈希函数的影响。这是至关重要的,因为HashMap使用二次幂长度哈希表,否则会出现哈希码在较低位不同的碰撞。注意:空键始终映射到哈希值0,因此索引0。
如果您想查看实现过程,请查看HashMap类源代码。
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
仍然需要处理具有相同哈希值的键。 - Frédéric Dumont
tieBreakOrder
仍然会调用identityHashCode
进行排序。 - Jeffrey Bosboomfind
在非可比较情况下似乎会双向查找(最后的 else-if 和 else),但我需要通过调试器来确认。这就引出了一个问题,为什么在这种情况下需要排序呢? - Jeffrey Bosboom