【问题标题】:How should I map long to int in hashCode()?我应该如何在 hashCode() 中将 long 映射到 int?
【发布时间】:2011-05-01 23:57:09
【问题描述】:

我有一系列具有long 字段的对象,该字段的值唯一地标识了整个系统中的特定对象,很像 GUID。我已覆盖 Object.equals() 以使用此 id 进行比较,因为我希望它与对象的副本一起使用。现在我也想覆盖Object.hashCode(),这基本上意味着将我的long 映射到一些int 返回值。

如果我正确理解hashCode 的用途,它主要用于哈希表,因此需要均匀分布。这意味着,只需返回 id % 2^32 就足够了。仅此而已,还是我应该注意其他事情?

【问题讨论】:

  • 顺便说一句,即使您只想保留低 32 位,也不需要模运算。投射到int 就足够了:int hashCode = (int) id
  • @Grodriguez 抱歉,但这个答案很糟糕!它将导致许多对象具有相同的哈希码,从而产生各种哈希冲突。你总是想要均匀分布的哈希码。此外,公认的答案也不是最佳解决方案,因为 java 8 引入了更好的解决方案。请参考“Nathan”给出的答案,因为Long.hashcode(long) 不会在堆栈上创建新对象
  • @Neuron 任何将 64 位值映射到 32 位值的哈希函数都会“导致许多对象具有相同的哈希码”。根本没有办法避免这种情况。此外,不能保证(this.longValue()^(this.longValue()>>>32)) 产生的散列码比仅保留值的低 32 位更均匀。
  • @Grodriguez 是的,再次抱歉。你说的对。我没有意识到将 long 转换为 int 会包裹 int 而不会分别停留在 Integer.MAX_VALUEMIN_VALUE 上,我是否会期望演员实际上正在做..

标签: java algorithm hash


【解决方案1】:

您已正确理解hashCode 的用途。是的,均匀分布是可取的(尽管不是实际要求)。

我建议((id >> 32) ^ id)

上面的表达式:

  • 使用原始值的所有位,不预先丢弃任何信息。例如,根据您生成 ID 的方式,高位可能会更频繁地更改(或相反)。
  • 不会对具有更多 1(零)的值引入任何偏差,因为如果将两半与 OR (AND) 运算结合起来就会出现这种情况。

【讨论】:

  • +1。这几乎是为java.lang.Long 定义的h​​ashCode,尽管它使用>>> 而不是>>。我想知道(new Long(id)).hashCode(); 或类似的是否会得到很好的优化。
  • @Steve:在这种情况下>>>>> 之间没有区别,因为无论如何都会丢弃在移位期间引入的额外 32 位。
  • 为了快速解释 Grodriguez 的反应(因为每个人都必须从某个地方开始),负数用最高位的 1 表示。当为负数时,数字从0xffffffff 倒计时。 >> 是有符号位移,而 >>> 是无符号位移。有符号移位保留这个最高 1 位以保持负号,这样-0x10 >> 0x2 产生-0x4,而-0x10 >>> 0x2 产生0x3ffffffc。此外,-1 >>> 1 产生0x7ffffffflongs 的宽度是 ints 的两倍,因此下移一半的位只会影响前 32 位。
【解决方案2】:
int result = (int)((longVal >> 32) ^ longVal);

分布得更均匀,因为如果只有 long 值的高位发生了变化,模不会返回不同的值。

【讨论】:

    【解决方案3】:

    从 Java 8 开始你可以使用

    Long.hashCode(guid);
    

    对于旧版本的 Java,您可以使用以下内容:

    Long.valueOf(guid).hashCode();
    

    请注意,此解决方案为堆栈创建了一个新对象,而第一个没有(尽管 Java 很可能优化了对象的创建..)

    查看文档,两种方式都使用以下算法:

    (int)(this.longValue()^(this.longValue()>>>32))
    

    这些都是不错的解决方案,因为它们利用了 Java 库 - 总是更好地利用已经测试过的东西。

    【讨论】:

    • 这可能很昂贵,因为它需要创建对象(因此是 Guava 替代方案)。至于算法本身,唯一危险的时候是高低 32 位具有相关含义的时候。例如,对于将 32 位 x 和 y 坐标存储在单个 long 中的 Point 类来说,这将是一个可怕的哈希码。
    • 虚拟机完全可以优化对象创建。并不是说我想依赖它。
    • @TofuBeer:实际上我猜虚拟机不可能优化它(尽管我想知道编译器是否可以合法地优化它)。你有源/JLS/JVM 规范链接吗?我会对这个很感兴趣。 Java 开发人员(通常包括我自己)经常将优化问题委托给 VM,而实际上规范甚至阻止了这些优化的发生。此外,我们经常依赖可能但在实践中不会发生的优化。
    • @Mark:如果与 -XX:+DoEscapeAnalysis 标志一起使用,最近的 Sun/Oracle JVM 对此进行优化。 JVM 将能够注意到Long 实例不需要存在于语句之外,因此可以在堆栈上创建。此外,它将能够内联代码(它会注意到我们谈论的是Long,而不是另一个扩展Long 的类)。基于堆栈的创建和内联允许 JVM 知道的所有进一步优化。
    • @james 不确定 98.8% 的统计数据(这取决于您进行了多少次插入),但将此视为“生日悖论”问题 (en.wikipedia.org/wiki/Birthday_problem)。在 Mark 的示例中,只有 1024 个可能的值,在随机选择的值的 38 个散列之后发生冲突的可能性为 50%,在 98 个之后发生冲突的可能性为 99%。这就是散列如此困难的原因:对于性能敏感的散列,您需要相应地了解您的数据 + 哈希,或者在通用库的(甚至更难的)情况下,预测可能的用途 + 很好地混合这些位。
    【解决方案4】:

    如果你还没有使用Guava,这有点小问题,但 Guava 可以很好地使用do this for you

    public int hashCode() {
      return Longs.hashCode(id);
    }
    

    这相当于Long.valueOf(id).hashCode()

    return (int) (value ^ (value >>> 32));
    

    另外,如果你有其他值或对象是哈希码的一部分,你可以写

    return Objects.hashCode(longValue, somethingElse, ...);
    

    long 将自动装箱为 Long,因此您将获得正确的哈希码作为整体哈希码的一部分。

    【讨论】:

    • 我不会仅仅为此而引入一个全新的库,但我从未听说过 Guava,它似乎很有帮助,值得从更普遍的角度来看。谢谢!
    • @Hanno:是的,为了这件小事当然不值得。但它是一个很棒的库,拥有大量有用的功能!
    • 在过去的几年里我没有做太多的 Java,但是 Guava 很棒,它提供了很多有用的类来改进你的代码。
    【解决方案5】:

    (l >> 32) ^ l 在大多数情况下是一个很好的哈希码;特别是当多头具有均匀分布时。

    由于这是公认的答案,我发布此内容是为了澄清我的一些 cmets 什么时候它不是一个好的哈希码。

    我给出的例子是这样的 Point 类:

    public class Point {
        private final long coords; //x in high-bits, y in low
        public int getX() {
            return (int)(coords >> 32);
        }
        public int getY() {
            return (int)coords;
        }
        public int hashCode() {
            return (int)((coords >> 32) ^ (coords));
        }
    }
    

    这可能看起来有些做作,但有时您会将多个“字段”打包成一个长文件。

    所以coords 字段代表 x 的 32 位和 y 的 32 位。那么为什么这是一个问题呢?好吧,如果 x 和 y 中的每一个都均匀分布在它们各自的 32 位上,情况并非如此。但这在实践中不太可能。更有可能的是 X 和 Y 受某个数字的限制。假设是 1024,因为它是 2^10。这意味着最多设置每个 X 和 Y 的低 10 位:

    00000000 00000000 000000XX XXXXXXXX 00000000 00000000 000000YY YYYYYYYY
    

    有 2^20 (1024*1024) 种可能的组合。但是hashCode是做什么的呢?

      00000000 00000000 000000XX XXXXXXXX 
    ^ 00000000 00000000 000000YY YYYYYYYY
    -------------------------------------
    = 00000000 00000000 000000?? ????????
    

    最多有 2^10 (1024) 个可能的 hashCode 值,因为只有低 10 位可以是零以外的任何值。哈希值与实际值的比率为1024:(1024*1024)1:1024。因此,两个数字具有相同的哈希值的概率是 1/1024。

    现在让我们通过应用birthday problem 中的数学计算碰撞概率。设 p(n) 为具有 n 个值时至少会发生一次碰撞的概率。我们知道 p(1025+) = 1 因为只有 1024 个值。

    p(n) = 1 - (n! * (1024 choose n))/1024^n
    

    结果如下:

    n: p(n)
    1: 0.00000
    2: 0.00098
    3: 0.00293
    4: 0.00585
    5: 0.00973
    6: 0.01457
    ...
    38: 0.50096
    ...
    79: 0.95444
    ...
    148: 0.99999
    

    只有 38 个项目,可能会发生冲突。有 148 个项目,有 99.999% 的机会发生(至少一个)碰撞。有 148 个项目,每个项目有 7% 的机会与另一个项目发生碰撞。使用适当的散列函数,根据领域知识,这些数字很容易降到 0。

    换句话说,了解您的域以及实际情况如何发生是制作高性能哈希的关键。库函数试图在对您的域一无所知的情况下尽可能地做好工作,并且通常依赖于在实践中不会发生的数据分布。

    【讨论】:

    • 最终,这个答案与我原来的说法是正交的,即使用 x^y 作为 Point 类是一个合理的散列。您在这里的论点是不合理 +if+ x 和 y 限制为最大 1024。有效点,但与我的原始陈述不矛盾。
    • @james:这只是不必要的无知,这是我的观点。在实践中,一组点多久均匀分布在其域上?几乎从不。 Bloch 建议这种类型的 hashCode 配方是有原因的:somePrime * getX() + getY()。这不是很好,但主要是在不了解域的情况下尝试“不相关”数据。一般来说,这也是真正的Point2D 类的工作原理。
    • @james:顺便说一下,这与 x 和 y 以 2^30 为界同样相关,尽管对于 2^30,无论如何你都会期待大量的碰撞;你对此无能为力。选择 1024 只是因为它易于解释。
    【解决方案6】:

    Java 8 将 Long.hashCode(long) 添加到 JDK。

    以下代码可以产生更高的性能。此代码将计算减少到 32 位 int,而不是使用 64 位 long 进行计算。这可以对 32 位和更小的架构产生影响。 x86 机器上的 32 位进程可以将其优化为一条指令,只需对 2 个寄存器进行异或操作。

    return (int)(value ^ (value >>> 32));

    正如其他答案中所述,这确实有一个好的avalanche effect,因此可能导致冲突。可以使用加密哈希函数来确保高雪崩效应。但是,还有其他算法,例如Murmur Hash(更多information),具有非常好的雪崩效果,但不会消耗太多CPU时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-15
      • 2011-10-07
      • 2015-08-27
      • 1970-01-01
      • 2012-04-06
      • 1970-01-01
      相关资源
      最近更新 更多