1. 引言

Java 14 引入了 record(记录)这一概念,作为传递不可变数据对象的更高效方案。record 本质上是受限的类形式,类似枚举(Enum),仅包含最基本的构造方法和访问器(getter),专注于作为纯粹的数据载体传递不可变数据。

本文将探讨如何重写 record 默认的 hashCode()equals() 实现。

2. hashCode() 和 equals() 方法

Java 的 Object 类定义了 equals()hashCode() 方法。由于所有类都继承自 Object,它们默认具备这些方法的实现:

  • equals():默认通过对象标识(内存地址)判断相等性
  • hashCode():返回基于当前实例的整数值,需与 equals() 协同实现

record 作为 Java 的受限类形式,自带 equals()hashCode()toString() 的默认实现。我们可以像普通类一样实例化 record 并比较相等性。

3. record 的默认实现

任何 record 类型都直接继承自 java.lang.Record,Java 为其提供了默认实现:

默认 equals() 特性

  • 遵循 Object.equals() 约定
  • 关键规则:通过复制所有属性创建的新 record 实例必须与原实例相等
  • 示例:
    public record Person(String firstName, String lastName, String SSN, String dateOfBirth) {}; 
    
    Person rob = new Person("Robert", "Frost", "HDHDB223", "2000-01-02");
    Person mike = new Person("Mike", "Adams", "ABJDJ2883", "2001-01-02");
    
    ✅ record 自动提供全参构造函数,无需手动编写

默认 equals() 测试

@Test
public void givenTwoRecords_whenDefaultEquals_thenCompareEquality() {
    Person robert = new Person("Robert", "Frost", "HDHDB223", "2000-01-02");
    Person mike = new Person("Mike", "Adams", "ABJDJ2883", "2001-01-02");
    assertNotEquals(robert, mike);
}

默认 hashCode() 特性

  • 组合所有属性的哈希值生成结果
  • 相同组件创建的 record 必须返回相同哈希码
  • 示例:
    @Test
    public void givenTwoRecords_hashCodesShouldBeSame() {
        Person robert = new Person("Robert", "Frost", "HDHDB223", "2000-01-02");
        Person robertCopy = new Person("Robert", "Frost", "HDHDB223", "2000-01-02");
        assertEquals(robert.hashCode(), robertCopy.hashCode());
    }
    

4. 自定义实现

Java 允许重写默认实现。例如:对于 Movie record,我们可能认为只要标题和发行年份相同即视为相等。

自定义 equals() 实现

record Movie(String name, Integer yearOfRelease, String distributor) {

    @Override
    public boolean equals(Object other) {
        if (this == other) return true;
        if (other == null) return false;
        if (!(other instanceof Movie)) return false;
        Movie movie = (Movie) other;
        return movie.name.equals(this.name) && 
               movie.yearOfRelease.equals(this.yearOfRelease);
    }
}

实现要点:

  1. 同一对象直接返回 true
  2. null 检查
  3. 类型检查
  4. 核心属性比较(nameyearOfRelease

自定义 hashCode() 实现

⚠️ 必须与 equals() 协同重写,否则哈希冲突时会导致错误:

@Override
public int hashCode() {
    return Objects.hash(name, yearOfRelease);
}

测试自定义实现

@Test
public void givenTwoRecords_whenCustomImplementation_thenCompareEquality() {
    Movie movie1 = new Movie("The Batman", 2022, "WB");
    Movie movie2 = new Movie("The Batman", 2022, "Dreamworks");
    assertEquals(movie1, movie2);  // 仅比较 name 和 yearOfRelease
    assertEquals(movie1.hashCode(), movie2.hashCode());
}

5. 何时需要重写

虽然 Java 规范要求自定义实现需满足「副本与原对象相等」的规则,但通常默认实现已足够。但需注意以下场景

⚠️ record 是浅不可变(shallowly immutable)

  • 当 record 包含可变属性(如 List)时,默认实现可能失效
  • 示例问题:
    record DataContainer(List<String> items) {}
    
    List<String> list1 = new ArrayList<>(List.of("A", "B"));
    List<String> list2 = new ArrayList<>(List.of("A", "B"));
    
    DataContainer c1 = new DataContainer(list1);
    DataContainer c2 = new DataContainer(list2);
    
    // 默认实现返回 false(因为 List 是不同实例)
    assertFalse(c1.equals(c2)); 
    

重写的必要性

  1. 包含可变对象时:需自定义比较逻辑
  2. 作为 Map 键时:必须保证 hashCode()equals() 的一致性
  3. 业务语义需求:仅需部分属性判断相等性(如前述 Movie 示例)

6. 结论

本文深入探讨了 record 的 hashCode()equals() 机制:

  • 默认实现已满足多数不可变数据场景
  • 自定义实现需遵循「副本相等」原则
  • ⚠️ 处理可变属性时必须重写,避免踩坑

所有示例代码可在 GitHub 仓库 获取。