你需要为记录类型覆盖hashCode()和equals()方法吗?

52

假设以下示例:

public record SomeRecord(int foo, byte bar, long baz)
{ }

如果我要将对象添加到HashMap中,我是否需要覆盖hashCodeequals方法?


5
不,我相当确定编译器会为您完成这个任务。请参阅此文章:https://aboullaite.me/java-14-records/。 javap 显示 equals、hashCode、toString、getter 和 setter 都已经为您生成了。这非常棒。 - user
5
这是“可能的”,因为在某些边角情况下,你需要这样做,但一般情况下,你不需要这样做,也许也不想这样做。如果确实需要,请确保实现了java.lang.Record中概述的equals()hashCode()的精细语义。 - Brian Goetz
2
@BrianGoetz 当您在这里时,是否有什么原因使得像record这样大声呼喊“值类型”的功能具有公共构造函数,就像IntegerDouble等的构造函数一样被标记为过时,而不是使用工厂方法允许在JRE自行决定的情况下进行共享,例如valueOf(…) - Holger
1
@Holger 是的,但我怀疑那是一个更长的对话。你能把它带到OpenJDK amber-dev列表吗? - Brian Goetz
3个回答

46
不,你不需要定义自己的hashCodeequals方法。如果你希望覆盖默认实现,可以这样做。
请参阅规范的第8.10.3节以获取详细信息https://docs.oracle.com/javase/specs/jls/se14/preview/specs/records-jls.html#jls-8.10
特别注意,在实现自己版本时要注意以下事项:
引用如下:

所有从java.lang.Record继承的成员。除非在记录体中显式地进行了覆盖,否则R将隐式声明覆盖了java.lang.Record的equals、hashCode和toString方法。

如果在记录体中显式声明了来自java.lang.Record的任何这些方法,则实现应满足java.lang.Record中指定的预期语义。

特别是,自定义的equals实现必须满足预期的语义,即记录的副本必须等于该记录。

3
它会计算所有记录组件的哈希码。 - Johannes Kuhn
1
@sc0der 技术上讲,Record 如何实现 hashCode 是留给实现的细节(记住 Java 是一种语言规范)。然而在实践中,我期望实现者通常只会哈希所有成员。冲突是在集合的实现中处理的。HashMap 的 openjdk 实现具有相当复杂的处理方式,根据冲突数量改变结构。 - sprinter
2
一个有效(但愚蠢)的hashCode()实现是return 1;。它遵守Object.hashCode()和Record.hashCode()的契约。(有一个标志可以使System.identityHashCode(Object)始终返回1:-XX:hashCode=2 - Johannes Kuhn
1
请参阅Record的Javadoc注释中的说明:实现要求: 隐式提供的实现返回一个哈希码值,该值通过组合所有组件的哈希码值得到,对于类型为引用类型的组件,使用Object.hashCode()方法,对于类型为原始类型的组件,使用相应的包装器类型的哈希码。 - Johannes Kuhn
1
@DidierA。我不相信有任何内置的注释可以从生成的equalshashCode中排除记录组件。您需要自己编写方法来实现。 - sprinter
显示剩余4条评论

6
关于您是否需要它的答案真的是 - 取决于您决定创建为Record的实体的实现方式。编译时或运行时都没有限制来约束您这样做,而对于扩展Object的类来说,这一直是如此。
另一方面,提议的主要motivations for the proposal之一是“低价值、重复性、容易出错的代码:构造函数、访问器、equals()hashCode()toString()等”。在数据载体中,这在今天的Java编程中相当常见。因此,进一步说明的决定是更喜欢语义目标。

...:将数据建模为数据。(如果语义正确,样板文件将自己处理)。声明浅不可变、行为良好的名义数据聚合应该是简单、清晰和简洁的。

因此,样板已经处理完毕,但请注意,您可能仍然希望其中一个记录组件不被视为在两个不同对象之间进行比较的一部分,这就是您可能想要覆盖所提供的“equals”和“hashCode”的默认实现的地方。此外,我毫不怀疑有时会需要一些 toString 的花哨效果,因此需要对其进行重载。
上述大多数不能归类为编译时或运行时失败,但提案本身表明了它所伴随的风险:
任何从状态描述中自动派生的成员也可以被显式声明。然而,粗心地实现访问器或equals/hashCode可能会破坏记录的语义不变量。
(注:后者主要是我的观点,即消费者希望拥有各种灵活性,以便他们可以使用最新的功能,但以现有实现方式工作。你知道,向后兼容性在升级过程中也非常重要。)

3

什么是Java Record? Java最常见的抱怨之一是,为了使类有用,需要编写大量代码。通常需要编写以下内容:

  1. toString()
  2. hashCode()
  3. equals()
  4. Getter方法
  5. 公共构造函数

对于简单的领域类,这些方法通常是无聊、重复的事情,很容易机械地生成(IDE经常提供此功能),但目前,语言本身没有提供任何方法来实现这一点。

记录的目标是扩展Java语言语法,创建一种方式来表明一个类是“字段,只是字段,而且仅仅是字段”。通过您对类做出这种声明,编译器可以帮助自动创建所有方法,并使所有字段参与hashCode()等方法。

记录会为记录内部的所有属性提供hashCode()equals()toString()的默认实现

hashCode()的默认实现

记录将使用记录内部所有属性的哈希码

equals()的默认实现

记录将使用所有属性来决定两个记录是否相等

因此,任何哈希实现,例如HashSet、LinkedHashSet、HashMap、LinkedHashMap等,都将使用hashCode(),并在发生任何冲突时使用equals()

默认实现还是自定义实现?

如果要在hashCode()equals()中使用所有属性,则不需要覆盖

需要为记录覆盖hashCode()和equals()吗?

保留默认实现还是选择仅选取某些属性取决于您

任何东西,但如果您想要自定义属性,则可以覆盖以决定哪些属性决定相等性和属性生成hashCode

默认实现如何计算hashCode?

将使用整数和字符串的哈希码,如下所示:

    int hashCode = 1 * 31;
    hashCode = (hashCode + "a".hashCode()) & 0x7fffffff;

以下代码是hashCode(), equals()toString()默认实现
由于这两个记录具有相同的hashCode和equals,因此HashSet没有将它们添加进去。
    static record Record(int id, String name) {

    }

    public static void main(String[] args) {
        Record r1 = new Record(1, "a");
        Record r2 = new Record(1, "a");

        Set<Record> set = new HashSet<>();
        set.add(r1);
        set.add(r2);
        System.out.println(set);

        System.out.println("Hashcode for record1: " + r1.hashCode());
        System.out.println("Hashcode for record2: " + r2.hashCode());

        int hashCode = 1 * 31;
        hashCode = (hashCode + "a".hashCode()) & 0x7fffffff;
        System.out.println("The hashCode: " + hashCode);
    }

输出

[Record[id=1, name=a]]
Hashcode for record1: 128
Hashcode for record2: 128
The hashCode: 128

, Resources:

blogs oracle


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接