为啥 Java 8 中的哈希映射使用二叉树而不是链表? [关闭]

Posted

技术标签:

【中文标题】为啥 Java 8 中的哈希映射使用二叉树而不是链表? [关闭]【英文标题】:Why hash maps in Java 8 use binary tree instead of linked list? [closed]为什么 Java 8 中的哈希映射使用二叉树而不是链表? [关闭] 【发布时间】:2016-06-23 15:02:21 【问题描述】:

我最近了解到,在 Java 8 中,哈希映射使用二叉树而不是链表,并且哈希码用作分支因子。我知道在发生高冲突的情况下,查找从 O(log n) 减少到O(n) 通过使用二叉树。我的问题是它真的有什么好处,因为摊销时间复杂度仍然是 O(1),也许如果你通过提供相同的哈希码来强制将所有条目存储在同一个桶中我们可以看到所有键都存在显着的时差,但没有人会这样做。

二叉树也比单链表使用更多的空间,因为它同时存储了左右节点。除了一些虚假的测试用例之外,时间复杂度绝对没有改善,为什么还要增加空间复杂度。

【问题讨论】:

这不是辩论。我在引用:我的问题是它真的有什么好处 [...] 也许如果您 [...] 我们可以看到显着的时差,但没有人在他们正常的头脑中会这样做。我>。因此,您是说它旨在处理没有头脑正常的人会做的案件,因此开发人员明确处理了一个愚蠢的案例,因此开发人员是愚蠢的并且您持有真相。我建议改写你的问题。 你的问题毫无意义。如果您假设不会发生哈希冲突,那么二叉树确实不会消耗更多空间,因为二叉树只会在存在 哈希冲突时使用。更准确地说,在实现从链表切换到二叉树之前,冲突的数量必须超过一个阈值。 @Tunaki 我特别指的是人们故意尝试这种情况以显示优势,如果我认为没有更多的东西,我永远不会问这个问题。 @Hoger 很好,我不知道链表也被使用了! 【参考方案1】:

这主要是与安全相关的更改。虽然在正常情况下很少可能发生多次冲突,但如果哈希键来自不受信任的来源(例如,从客户端接收的 HTTP 标头名称),那么特制输入是可能的并且不是很难,因此生成的键将具有相同的哈希码。现在,如果您执行许多查找,您可能会遇到拒绝服务。看起来有相当多的代码很容易受到这种攻击,因此决定在 Java 端解决这个问题。

更多信息请参考JEP-180。

【讨论】:

同意,但用户如何在不知道底层哈希函数的情况下设计输入以增加碰撞? @user1613360,String 和documented 的哈希函数是众所周知的,所以问题不大。每个 Java 实现都必须使用相同的函数。 除了某些哈希码是固定的并且有据可查的事实之外,攻击者总是可以尝试找出服务器正在运行的特定版本,并查看特定类实现的哈希码是如何计算的。这就是大多数攻击的工作方式,首先,找出运行的软件,然后尝试针对特定软件和版本量身定制的适当漏洞利用。 相关:http://www.javaspecialists.eu/archive/Issue235.html @AnmolSinghJaggi 如果 hashCode 完全相同,它将不起作用。在有针对性的 DoS 攻击的情况下,它很容易生成,例如具有完全相同 hashCode 的字符串键。【参考方案2】:

您的问题包含一些错误的前提。

    桶冲突不一定是哈希冲突。您不需要为两个对象使用相同的哈希码就可以在同一个存储桶中结束。桶是数组的一个元素,哈希码必须映射到特定的索引。由于数组大小相对于Map 的大小应该是合理的,因此您不能随意提高数组大小以避免桶冲突。甚至存在理论上的限制,即数组大小最大为 2³¹,而可能的哈希码为 2³²。 发生哈希冲突并不表示编程不当。对于所有值空间大于 2³² 的对象,具有相同哈希码的不同对象的可能性是不可避免的。 Strings 是一个明显的例子,但即使是带有两个 int 值的 Point 或普通的 Long 键也会不可避免地发生哈希冲突。因此它们可能比您想象的更常见,这在很大程度上取决于用例。 仅当存储桶中的冲突数量超过某个阈值时,实施才会切换到二叉树,因此更高的内存成本仅适用于它们将得到回报的情况。关于它们的工作方式,似乎存在一个普遍的误解。由于桶冲突不一定是哈希冲突,所以二分查找会首先搜索哈希码。只有当哈希码相同并且密钥适当地实现Comparable时,才会使用其自然顺序。您可能在 Web 上找到的示例故意使用相同的对象哈希码来演示 Comparable 实现的使用,否则不会出现。他们触发的只是实施的最后手段。 作为Tagir pointed out,此问题可能会影响软件的安全性,因为缓慢的回退可能会打开 DoS 攻击的可能性。在以前的 JRE 版本中已经多次尝试解决这个问题,但这些问题比二叉树的内存消耗有更多的缺点。例如,在 Java 7 中尝试将哈希码随机映射到数组条目,这导致了 this bug report 中记录的初始化开销。新的实现并非如此。

【讨论】:

相关:http://www.javaspecialists.eu/archive/Issue235.html 在这里指出“桶碰撞”不一定与“哈希碰撞”相同,这一点很重要。许多程序员错误地认为它们是同义词。 对此我有一个疑问。对于 Hashmap,实现可比较的 key 不是强制性的。那么,当我们切换到基于二叉树的 hashMap 时,我们是否假设该 key 实现了可比较性。 @Akanksha 也许你没有仔细阅读这个答案,尤其是第三点。通过将受影响的存储桶转换为哈希码上的二叉树,可以解决大量 存储桶冲突。这并不要求密钥具有可比性,哈希码是可比的。仅对于大量实际哈希冲突,即相同的哈希码,使用使用可比较实现的二叉树,如果键是可比较的,否则不是。

以上是关于为啥 Java 8 中的哈希映射使用二叉树而不是链表? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

JDK8的HashMap为啥要引入红黑树?

为什么Java8中HashMap链表使用红黑树而不是AVL树

将空二叉树填充为二叉搜索树而不改变结构(节点链接)

Java全数据结构与部分算法(看到就是赚到)

在啥情况下我应该使用尝试而不是二叉树/哈希表? [复制]

二叉树和哈希表的优缺点对比与选择