哈希映射调整方法大小实现详细信息

2022-09-01 17:49:59

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

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

通常,也可以从其他地方调用,但让我们只看这种情况。resize

假设您将这些字符串作为键放在 a 中(右侧是 after - 这是内部重新哈希)。是的,这些都是精心生成的,而不是随机的。HashMaphashcodeHashMap#hash

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

这里有一个简单的模式需要注意 - 最后4位对于所有键都是相同的 - 这意味着当我们插入其中的8个键(总共有9个)时,它们将最终在同一桶中;在9日,将被称为。HashMap#putresize

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

我们把第九个键。此时被击中并被调用。箱子加倍到,键多一位决定该条目将去哪里(所以,现在是5位)。TREEIFY_THRESHOLDresize32

最终到达这段代码(当发生时):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位)是否实际上为零 - 如果是,则意味着此条目将保留在原处;如果不是,它将在新箱中以2的幂偏移移动。

现在终于有一个问题了:调整大小时的那段代码是精心制作的,以便它保留该箱中条目的顺序

因此,在您将这9个键放入顺序之后,顺序将是:HashMap

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

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

为什么要保留 中某些条目的顺序。a中的顺序非常糟糕,如这里这里所详述的那样。HashMapMap


答案 1

设计注意事项已记录在同一源文件中,在第 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


答案 2

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

一个是你通过增加(或减少)哈希值来维持秩序。这意味着在搜索条柱时,只要当前项目大于(或小于)正在搜索的哈希值,您就可以停止。

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

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

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

虽然您确实不应该依赖不保证它的容器的迭代顺序,但这并不意味着如果它是结构性的,则不能利用它来提高性能。另请注意,类的实现具有特权,可以以该类的用户不应采用的形式方式利用其实现的详细信息。

如果您查看源代码并了解其实现方式并利用它,那么您就冒了风险。如果实现者这样做,那就是另一回事了!

注意:我有一个算法的实现,该算法严重依赖于名为Hashlife的哈希表。它使用这个模型,有一个2的幂的哈希表,因为(a)你可以通过位掩码(&mask)而不是除法来获取条目,(b)重新哈希是简化的,因为你只有每个“解压缩”哈希箱。

基准测试表明,算法通过在访问时主动将模式移动到其箱子的前面来获得约20%的收益。

该算法几乎利用了元胞自动机中的重复结构,这是很常见的,所以如果你已经看到了一个模式,再次看到它的机会很高。