【问题标题】:Why does a HashMap rehash the hashcode supplied by the key object?为什么 HashMap 会重新散列键对象提供的哈希码?
【发布时间】:2010-03-29 13:14:08
【问题描述】:

我正在阅读 Java 1.6 API 提供的 HashMap 类的代码,无法完全理解以下操作的需要(在 put 和 get 方法的主体中找到):

int hash = hash(key.hashCode());

方法hash() 具有以下主体:

 private static int hash(int h) {
         h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

这通过对提供的哈希码执行位操作来有效地重新计算哈希。即使 API 声明如下,我也无法理解这样做的必要性:

这很关键 因为 HashMap 使用长度为二的幂的哈希表,所以 否则会遇到没有不同的 hashCodes 冲突 在低位。

我确实了解键值 par 存储在数据结构数组中,并且该数组中项目的索引位置由其哈希确定。 我不明白的是这个函数如何为哈希分布增加任何价值。

【问题讨论】:

    标签: java collections hashmap hash hashcode


    【解决方案1】:

    正如 Helper 所写,它的存在只是为了以防关键对象的现有散列函数出现故障,并且在混合低位方面做得不够好。根据pgras引用的the source

     /**
      * Returns index for hash code h.
      */
     static int indexFor(int h, int length) {
         return h & (length-1);
     }
    

    哈希以 2 的幂的长度进行与运算(因此,length-1 保证为 1 的序列)。由于此 ANDing,仅使用了 h 的低位。 h 的其余部分被忽略。想象一下,无论出于何种原因,原始哈希仅返回可被 2 整除的数字。如果直接使用它,则永远不会使用哈希图的奇数位置,从而导致碰撞次数增加 x2。在一个真正病态的情况下,一个糟糕的哈希函数会使哈希图表现得更像一个列表,而不是一个 O(1) 容器。

    Sun 工程师必须运行测试表明,太多哈希函数的低位不够随机,而且许多哈希图不够大,无法使用高位。在这些情况下,HashMap 的hash(int h) 中的位操作可以提供比大多数预期用例(由于较低的冲突率)的净改进,即使需要额外的计算。

    【讨论】:

    • “以防万一”?实际上,Java 中的大多数哈希码都会很糟糕。例如,看看 java.lang.Integer!但这实际上是有道理的。最好说“如果每个人的 Object.hashCode()s 有糟糕的位分布也没关系,只要他们遵循 equal-objects-have-equal-hashcodes 规则,并尽量避免冲突。”然后只有像 HashMap 这样的集合实现有责任通过辅助哈希函数传递这些值,而不是每个人的问题。
    • 'hashmap的奇数位置永远不会被使用' 没看懂,能举个例子吗?
    • 好吧,假设我正在散列 Employee 对象,我所有的员工都有一个 int ID 字段,例如“400114”、“400214”、“400314”等(它们都共享“14 “他们的 ID 的一部分,因为那是我部门的后缀)。 Integer 的 hashCode() 方法返回整数本身——因此,如果我使用员工 ID 作为 HashSet /without/ HashMap 的 hash(int h) 中的键,则分布将非常非常不均匀。在这个例子中,因为 14 是偶数,所以只会使用偶数桶。
    • @tucuxi 所以我可以认为hash(int h) 是用于均匀分布的次要散列吗??
    【解决方案2】:

    我在某处读到这样做是为了确保良好的分发,即使您的 hashCode 实现,嗯,错误,很糟糕。

    【讨论】:

    • 对,java.lang.Object中默认的hashcode()实现在hash之间没有太多分布。
    • 我不明白的是,如果每个散列都是唯一的(并且所讨论的方法没有 - 也不能 - 解决唯一散列的问题),该机制面临什么问题?它提到了一些关于低位冲突的事情——但这不是很清楚。
    • 根据定义,每个哈希都不是唯一的...我无法很好地回答您的问题,但问题在于返回“hashCode & (length-1)”的“indexFor”方法。 .
    【解决方案3】:

    正如您所知道的哈希图,底层实现是一个哈希表,特别是一个封闭的桶哈希表。负载因子决定了集合中合适的对象数量/桶的总数。

    假设您不断添加更多元素。每次你这样做时,它不是更新,它会运行对象的 hashcode 方法,并使用带模运算符的桶数来决定对象应该进入哪个桶。

    随着 n(集合中的元素数)/m(桶数)变大,您的读写性能会越来越差。

    假设您的哈希码算法非常出色,性能仍取决于此比较 n/m。

    重新散列也用于更改存储桶的数量,并且仍然保持与构建集合相同的负载因子。

    请记住,任何散列实现的主要好处是读取和写入的理想 O(1) 性能。

    【讨论】:

      【解决方案4】:

      如您所知,object.hashCode() 可以被用户覆盖,所以一个非常糟糕的实现会抛出非随机的较低级别位。这往往会挤满一些水桶,并导致许多水桶未装满。

      我刚刚创建了一张他们在哈希中尝试做什么的可视化地图。似乎 hash(int h) 方法只是通过进行位级操作来创建一个随机数,以便生成的数字更随机(因此更均匀地进入存储桶)分布。

      每个位都重新映射到不同的位,如下所示:

              h1 = h1 ^ h13 ^ h21 ^ h9 ^ h6     
              h2 = h2 ^ h14 ^ h22 ^ h10 ^ h7
              h3 = h3 ^ h15 ^ h23 ^ h11 ^ h8
              h4 = h4 ^ h16 ^ h24 ^ h12 ^ h9
              h5 = h5 ^ h17 ^ h25 ^ h13 ^ h10
      

      。 . . .

      直到 12 点。

      如你所见,h 的每一位都会离它自己那么远。所以这将是非常随机的,不会挤满任何特定的桶。希望这有帮助。如果您需要完整的视觉效果,请给我发送电子邮件。

      【讨论】:

        猜你喜欢
        • 2015-01-13
        • 2019-07-09
        • 2018-05-30
        • 2015-08-12
        • 2013-06-20
        • 2013-08-28
        • 2012-12-25
        • 2014-05-21
        • 1970-01-01
        相关资源
        最近更新 更多