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 实例必须与原实例相等
- 示例:
✅ 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");
默认 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);
}
}
实现要点:
- 同一对象直接返回
true
null
检查- 类型检查
- 核心属性比较(
name
和yearOfRelease
)
自定义 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));
重写的必要性
- 包含可变对象时:需自定义比较逻辑
- 作为 Map 键时:必须保证
hashCode()
和equals()
的一致性 - 业务语义需求:仅需部分属性判断相等性(如前述
Movie
示例)
6. 结论
本文深入探讨了 record 的 hashCode()
和 equals()
机制:
- 默认实现已满足多数不可变数据场景
- 自定义实现需遵循「副本相等」原则
- ⚠️ 处理可变属性时必须重写,避免踩坑
所有示例代码可在 GitHub 仓库 获取。