HashMap调整方法实现细节

时间:2017-07-30 20:51:30

标签: java java-8 hashmap hashcode

正如标题所示,这是一个关于HashMap#resize的实现细节的问题 - 当内部数组的大小加倍时。 这有点罗嗦,但我真的试图证明我对此有了最好的理解......

这种情况发生在此特定桶/箱中的条目以Linked方式存储时 - 因此具有确切的顺序并且在问题的上下文中这很重要

一般来说resize也可以从其他地方调用,但我们只看这个案例。

假设您将这些字符串作为键放在HashMap中(右侧是 hashcode之后的HashMap#hash - 这就是内部重新散列。)是的,这些是精心生成的,而非随机的。

 DFHXR - 11111
 YSXFJ - 01111 
 TUDDY - 11111 
 AXVUH - 01111 
 RUTWZ - 11111
 DEDUC - 01111
 WFCVW - 11111
 ZETCU - 01111
 GCVUR - 11111 

这里有一个简单的模式 - 最后4位对于所有这些都是相同的 - 这意味着当我们插入这些键中的8个(总共9个)时,它们将在同一个桶中结束;在第9 HashMap#put处,resize将被调用。

因此,如果当前在HashMap中有8个条目(具有上述键之一) - 这意味着此映射中有16个桶,它们的最后4个键决定了条目最终的位置。

我们把第九个键。此时TREEIFY_THRESHOLD被点击,resize被调用。这些容器加倍到32,并且来自键的另一位决定了该条目的去向(现在,5位)。

最终到达这段代码(当resize发生时):

 Node<K,V> loHead = null, loTail = null;
 Node<K,V> hiHead = null, hiTail = null;
 Node<K,V> next;
 do {
     next = e.next;
     if ((e.hash & oldCap) == 0) {
          if (loTail == null)
               loHead = e;
          else
               loTail.next = e;
          loTail = e;
     }
     else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
     }
 } while ((e = next) != null);



 if (loTail != null) {
     loTail.next = null;
     newTab[j] = loHead;
 }
 if (hiTail != null) {
     hiTail.next = null;
     newTab[j + oldCap] = hiHead;
 }

它实际上并不复杂......它的作用是将当前垃圾箱拆分为将移动到其他垃圾箱的条目以及不会移动到其他垃圾箱的条目垃圾箱 - 但肯定会留在这个垃圾箱里。

它实际上非常聪明,它是通过这段代码来实现的:

 if ((e.hash & oldCap) == 0) 

这样做是检查下一位(在我们的例子中是第5位)是否实际为零 - 如果是,则表示此条目将保持原样;如果不是,它将以新箱中的两个偏移力移动。

最后问题是:调整大小中的那段代码是经过精心设计的,以便保留该bin中条目的顺序

因此,在将{9}键放入HashMap之后,订单将会是:

DFHXR -> TUDDY -> RUTWZ -> WFCVW -> GCVUR (one bin)

YSXFJ -> AXVUH -> DEDUC -> ZETCU (another bin)

为什么要保留HashMap中某些条目的顺序。 Map中的订单确实错误,详细信息为herehere

3 个答案:

答案 0 :(得分:3)

设计注意事项已记录在同一源文件中的line 211的代码注释中

* When bin lists are treeified, split, or untreeified, we keep 
* them in the same relative access/traversal order (i.e., field 
* Node.next) to better preserve locality, and to slightly 
* simplify handling of splits and traversals that invoke 
* iterator.remove. When using comparators on insertion, to keep a 
* total ordering (or as close as is required here) across 
* rebalancings, we compare classes and identityHashCodes as 
* tie-breakers. 

由于通过迭代器删除映射无法触发调整大小,因此保留resize中特定顺序的原因是“更好地保留局部性,并稍微简化分割的处理”,以及保持一致有关该政策。

答案 1 :(得分:1)

  

地图中的订单非常糟糕[...]

它不错,它(在学术术语中)无论如何。斯图尔特马克斯在你发布的第一个链接上写道:

  

[...]为未来的实施变更保留灵活性[...]

这意味着(据我所知)现在实施恰好保留了订单,但是如果找到更好的实现,将来它将用于保持订单。

答案 2 :(得分:1)

维护作为链接列表实现的bin中的顺序有两个常见原因:

一个是通过增加(或减少)哈希值来维持秩序。 这意味着在搜索bin时,只要当前项目比搜索的哈希值更大(或更少,如果适用),就可以停止。

另一种方法涉及在访问时将条目移动到桶的前面(或更靠近前面),或者只是将它们添加到前面。这适用于刚访问过元素的概率很高的情况。

我已经查看了JDK-8的源代码,它似乎(至少在大多数情况下)执行后来的被动版本(添加到前面):

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/HashMap.java

虽然你永远不应该依赖不能保证它的容器的迭代顺序,但这并不意味着它不能被利用来提高性能#&# 39; s结构。还要注意,类的实现处于特权位置,以正式方式利用其实现的细节,该类的用户不应该这样做。

如果您查看来源并了解其实施和利用方式,您就会承担风险。如果实施者这样做,那就不同了!

注意: 我有一个算法的实现,它严重依赖于一个名为Hashlife的哈希表。使用这个模型,有一个哈希表,其功能为2,因为(a)你可以通过位掩码(&amp; mask)而不是除法来获得条目,并且(b)简化rehashing因为你只是每一个解压缩&#39;哈希二进制位。

基准测试显示,通过在访问时将模式主动移动到其垃圾箱的前面,算法获得了大约20%的收益。

该算法几乎利用了细胞自动机中的重复结构,这是常见的,因此如果您已经看到模式,那么再次看到它的可能性很高。