为什么Java 8中的哈希映射使用二叉树而不是链表?

时间:2016-03-09 09:54:14

标签: java hashmap java-8 theory

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

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

2 个答案:

答案 0 :(得分:22)

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

有关更多信息,请参阅JEP-180

答案 1 :(得分:17)

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

  1. 存储桶冲突不一定是哈希冲突。您不需要为两个对象使用相同的哈希码来结束同一个存储桶。存储桶是数组的元素,并且哈希代码必须映射到特定索引。由于数组大小相对于Map的大小应该合理,因此不能任意提高数组大小以避免存储桶冲突。甚至理论上的限制是阵列大小最大可达2³¹,而有2 2,3可能的哈希码。
  2. 发生哈希冲突并不是编程错误的表现。对于值空间大于2³的所有对象,具有相同哈希码的不同对象的可能性是不可避免的。 String是一个明显的例子,但即使带有Pointint值的Long或普通Comparable密钥也会产生不可避免的哈希冲突。因此,它们可能比您想象的更常见,并且在很大程度上取决于用例。
  3. 只有当存储桶中的冲突数超过某个阈值时,实现才会切换到二叉树,因此较高的内存成本仅在付款时才适用。似乎有一个共同的误解,他们是如何工作的。由于桶冲突不一定是散列冲突,因此二进制搜索将首先搜索散列码。只有当哈希码相同且密钥适当地实现Comparable时,才会使用其自然顺序。您可能在Web上找到的示例故意使用相同的对象哈希代码来演示List<string> folders = new List<string>(); string folderResult = string.Join(",", ParallelMeasurements .Select(parallelMeasurement => parallelMeasurement.Item1.Substring(0, parallelMeasurement.Item1.IndexOf("."))) .Where(folder => folders.Contains(folder)) .Select(folder => { folders.Add(folder); return folder; })); string valueResult = string.Join(",", folders .Select(folder => ParallelMeasurements .Where(parallelMeasurement => parallelMeasurement.Item1.Substring(0, parallelMeasurement.Item1.IndexOf(".")) == folder)) .Select(views => views.Sum(view => view.Item2.TotalSeconds))); 实现的使用,否则将无法显示。他们触发的只是实施的最后手段。
  4. 作为Tagir pointed out,此问题可能会影响软件的安全性,因为缓慢的回退可能会导致DoS攻击的可能性。在以前的JRE版本中已经有几次尝试来解决这个问题,这个问题比二叉树的内存消耗有更多的缺点。例如,尝试将哈希码的映射随机化到Java 7中的数组条目,这导致初始化开销,如this bug report中所述。这种新实现并非如此。