【问题标题】:What is the reason behind Enum.hashCode()?Enum.hashCode() 背后的原因是什么?
【发布时间】:2025-12-31 03:55:07
【问题描述】:

Enum 类中的方法 hashCode() 是 final 的,定义为 super.hashCode(),表示它根据实例地址返回一个数字,是程序员 POV 的随机数。

定义它,例如因为ordinal() ^ getClass().getName().hashCode() 在不同的 JVM 中是确定性的。它甚至会更好一点,因为最低有效位会“尽可能多地改变”,例如,对于包含多达 16 个元素和大小为 16 的 HashMap 的枚举,肯定不会发生冲突(当然,使用 EnumMap 更好,但有时不可能,例如没有 ConcurrentEnumMap)。按照目前的定义,你没有这样的保证,是吗?

答案总结

使用Object.hashCode() 与上面类似的更好的hashCode 进行比较,如下所示:

  • 优点
    • 简单
  • 对比
    • 速度
    • 更多冲突(对于任何大小的 HashMap)
    • 不确定性,它会传播到其他对象,使其无法用于
      • 确定性模拟
      • ETag 计算
      • 寻找错误取决于例如在 HashSet 迭代顺序上

我个人更喜欢更好的 hashCode,但恕我直言,没有理由很重要,也许除了速度。

更新

我对速度很好奇,写了一个benchmark 和令人惊讶的results。对于每个类的单个字段的价格,您可以获得几乎快四倍的确定性哈希码。在每个字段中存储哈希码会更快,尽管可以忽略不计。

标准哈希码没有快多少的解释是它不能是对象的地址,因为对象被 GC 移动。

更新 2

going onhashCode 的性能一般有一些奇怪的东西。当我理解它们时,还有一个悬而未决的问题,为什么System.identityHashCode(从对象头读取)比访问普通对象字段慢得多。

【问题讨论】:

  • 我认为默认的hashCode() 实现没有问题。而且您不应该需要它在 JVM 之间具有确定性。
  • 好吧,他们已经选择了这样的字符串(以及原始包装类型)。
  • 使用Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));(在download.oracle.com/javase/6/docs/api/java/util/EnumMap.html上推荐)有什么问题吗?您将如何实施ConcurrentEnumMap
  • 我认为作者的观点是,您可能会发现自己比较不同虚拟机上相同 Enum 值的两个实例,其中每个实例具有不同的内存地址,因此具有不同的 hashCode()。有些答案似乎说这不可能发生,但是那些人尝试过吗?有了 Java EE 的所有特性以及在多台主机上的部署,您能证明这不会造成问题吗?
  • Enum hashCode 和 ordinal 相同的另一个优点是在分布式环境的 ETag(HTTP 响应标头)计算中使用 Enum hashCode。(ETag 需要在所有机器上保持一致)分发,否则 ETag 会有效地失去它的缓存功能)

标签: java enums hash


【解决方案1】:

我也问过同样的问题,因为没看到这个。 Why in Enum hashCode() refers to the Object hashCode() implementaion, instead of ordinal() function?

在为依赖枚举 hashCode 作为组合之一的对象定义我自己的哈希函数时,我遇到了一个问题。在检查函数返回的一组对象中的值时,我按顺序检查它们,我希望它是相同的,因为我自己定义了 hashCode,所以我希望元素落在相同的节点上在树上,但是由于枚举返回的hashCode从头到尾都在变化,所以这个假设是错误的,测试可能偶尔会失败。

所以,当我发现问题时,我开始改用序数。 我不确定每个为其对象编写 hashCode 的人都意识到这一点。

所以基本上,你不能定义自己的确定性 hashCode,而依赖于 enum hashCode,你需要使用 ordinal 来代替

附:这对于评论来说太大了:)

【讨论】:

    【解决方案2】:

    我可以想象这样实现它的另一个原因是因为 hashCode() 和 equals() 的要求是一致的,并且 Enums 的设计目标是它们应该易于使用和编译时常量(使用它们是“case”常量)。这也使得将枚举实例与“==”进行比较是合法的,并且您根本不希望“等于”的行为与枚举的“==”不同。这再次将 hashCode 与默认的 Object.hashCode() 基于引用的行为联系起来。 如前所述,我也不希望 equals() 和 hashCode() 将来自不同 JVM 的两个枚举常量视为相等。在谈论序列化时:例如类型为枚举的字段,Java 中的默认二进制序列化程序具有一种特殊行为,即仅序列化常量的名称,并且在反序列化时,会重新创建对反序列化 JVM 中相应枚举值的引用. JAXB 和其他基于 XML 的序列化机制以类似的方式工作。所以:别担心

    【讨论】:

      【解决方案3】:

      只要我们不能将枚举对象1 发送到不同的 JVM,我认为没有理由对枚举(以及一般对象)提出这样的要求


      1 我认为这很清楚 - object 是一个类的实例。 序列化对象是一个字节序列,通常存储在一个字节数组中。我说的是一个对象

      【讨论】:

      • 我们可以通过序列化将一个对象发送到不同的JVM。您也可以在那里创建相同的对象。但你不能指望相同的 hashCode,这是不必要的不​​确定性。
      • @maaartinus - 序列化不发送 the 对象,而是发送一些可用于重新创建更多或等效对象的数据。我们不能,这就是重点,将来自不同 jvm 的两个对象与obj1.equals(obj2) 进行比较。
      • 确实如此。但我没有说我们可以。我知道以这种方式定义的 hashCode 遵守合同,这不是我的意思。
      【解决方案4】:

      我可以想象使用 Object 的 hashCode() 并使其最终化的唯一原因是让我问这个问题。

      首先,您不应该依赖这种机制在 JVM 之间共享对象。这根本不是受支持的用例。当您序列化/反序列化时,您应该依靠自己的比较机制,或者仅将结果与您自己的 JVM 中的对象“比较”。

      让枚举hashCode 实现为Objects 哈希码(基于身份)的原因是,在一个JVM 中每个枚举对象只有一个实例。这足以确保这样的实现有意义且正确。

      你可以这样争论 “嘿,String 和原语的包装器(Long、Integer、...)都有明确的、确定性的 hashCode 规范!为什么枚举没有它?",首先,你可以有几个不同的字符串引用代表同一个字符串,这意味着使用super.hashCode 会出错,所以这些类必然需要它们自己的 hashCode 实现。对于这些核心类,让它们具有明确定义的确定性 hashCode 是有意义的。

      为什么他们选择这样解决?

      好吧,看看the requirements of the hashCode implementation。主要关心的是确保每个对象都应该返回一个 distinct 哈希码(除非它等于另一个对象)。基于身份的方法非常有效并保证了这一点,而您的建议却没有。这个要求显然比任何关于放宽序列化等的“便利奖金”都要强。

      【讨论】:

      • +1 很好的答案,虽然是针对不同的问题。你在告诉我一个原因,为什么这样做可以,但我在问,为什么选择这种方式。
      • "在一个 JVM 中,每个枚举对象只有一个实例" - 你确定吗?如果同一个枚举类被不同的类加载器加载怎么办?
      • @Oledzki:好点!但是枚举不再相等,因为 equals() 在 Java 6 中由 == 运算符实现,因此遵守哈希码协定。
      • @GrzegorzOledzki, 如果同一个枚举类被不同的类加载器加载会怎样? -- 它们是完全不同的类,因此枚举 java.lang.Class 完全 相同的 hashCode/equals,只是 b/c 它们的名称可能相同,它们不是相同的类。 Class.getName() 只是一个人类可读的名称,但对于内部表示并没有太多意义。简而言之,来自不同类加载器的类可能共享名称,但这无关紧要。
      • 为什么选择这种方式。 - 这是最好的实现。有 - 一个具有良好熵的随机数(可以混合不同的枚举),不需要额外的空间并且完全符合所有要求。实际上,其他任何事情都会低于标准。顺便说一句:相同类型的枚举可能不具有相同的类,因为覆盖方法会导致全新的类。
      【解决方案5】:

      JVM 强制对于枚举常量,内存中只存在一个对象。您不可能在单个 VM 中得到相同枚举常量的两个不同实例对象,而不是反射,也不能通过序列化/反序列化通过网络。

      话虽如此,既然它是唯一代表这个常量的对象,那么它的hascode就是它的地址并不重要,因为没有其他对象可以同时占用相同的地址空间。它保证是唯一的和“确定性的”(从某种意义上说,在同一个 VM 中,在内存中,所有对象都将具有相同的引用,无论它是什么)。

      【讨论】:

      • 与 aioobe 的回答相同的评论。您写道:“JVM 强制规定,对于枚举常量,内存中只存在一个对象”,当不同的类加载器加载同一个枚举类时,这似乎并不正确。
      • 你错了。对象个数可能超过2**32,所以hashCode不能唯一。
      • @Grzegorz:据我所知,一旦类加载器加载了一个类,并且在同一个应用程序中,另一个尝试查找该类,他将只使用已经加载了一个,不再加载它。 @maaartinus:我同意,但这通常是哈希码的一个缺陷。由于它们使用hashCode(),因此在结构的标准实现中无法避免。我猜,2^32 个或更多对象很少驻留在内存中,在单个哈希图中,这在某种程度上得到了缓解。依赖外部散列的自定义实现通过使用更长的数据类型来解决这个问题。
      • 只有在同一个类加载器尝试再次加载该类时才如此。但是您可能在应用程序中有多个(独立的)类加载器,其中一个可能不知道另一个。
      • 我已经在我的应用程序中引用了两个不同的枚举实例,所以我可以确认上面关于类加载器的评论是正确的。
      【解决方案6】:

      不要求 JVM 之间的哈希码是确定性的,如果是,则不会获得任何优势。如果你依赖这个事实,那么你就错了。

      由于每个枚举值只存在一个实例,Object.hashcode() 保证不会发生冲突,代码重用性好,速度非常快。

      如果相等性由身份定义,那么Object.hashcode() 将始终提供最佳性能。

      其他哈希码的确定性只是其实施的副作用。由于它们的相等性通常由字段值定义,因此混合使用非确定性值将浪费时间。

      【讨论】:

      • 你在速度方面完全错了,请参阅我的更新。
      【解决方案7】:

      我认为他们将其定为最终版本的原因是为了避免开发人员通过重写次优(甚至不正确)的 hashCode 来自取其辱。

      关于选择的实现:它在 JVM 中不稳定,但它非常快,避免冲突,并且不需要枚举中的额外字段。考虑到枚举类的实例数量通常很少,以及 equals 方法的速度,如果 HashMap 查找时间比当前算法的查找时间长,我不会感到惊讶,因为它具有额外的复杂性。

      【讨论】:

        最近更新 更多