将 类 转换为记录时出现兼容性问题

Compatibility issues while converting Classes to Records

我一直在与以下名为 City

的 class 合作
@ToString
@AllArgsConstructor
public class City {
    Integer id;
    String name;
}

并尝试将其转换为 record 称为 CityRecord as

record CityRecord(Integer id, String name) {} // much cleaner!

但是转向这样的表示,我们的一个单元测试开始失败。这些测试在内部处理从 JSON 文件中读取的城市列表,并映射到一个对象,进一步计算城市数量,同时将它们分组到 Map 中。简化为:

List<City> cities = List.of(
        new City(1, "one"),
        new City(2, "two"),
        new City(3, "three"),
        new City(2, "two"));
Map<City, Long> cityListMap = cities.stream()
        .collect(Collectors.groupingBy(Function.identity(),
                Collectors.counting()));

以上代码断言 true 包含 4 个键,每个键出现 1 次。对于记录表示,结果 Map 中的键不超过 3 个。造成这种情况的原因是什么?解决这个问题的方法是什么?

原因

观察到的行为背后的原因如 java.lang.Record

中所述

For all record classes, the following invariant must hold: if a record R's components are c1, c2, ... cn, then if a record instance is copied as follows:

 R copy = new R(r.c1(), r.c2(), ..., r.cn());   then it must be the case that r.equals(copy).

简而言之,您的 CityRecord class 现在有一个 equals(和哈希码)实现来比较两个属性并确保它们是否相等,由这些组件组成的记录是也相等。作为此评估的结果,具有相同属性的两个记录对象将组合在一起。

因此,结果 infer/assert 应该是三个这样的键,其中一个 id=2, name="two" 计算了两次。

立即补救

一个直接的临时解决方案是在您的记录表示中创建自定义(有缺陷 - 原因稍后解释)equals 实现。这看起来像:

record CityRecord(Integer id, String name) {

    // WARNING, BROKEN CODE
    // Does not adhere to contract of `Record::equals`
    @Override
    public boolean equals(Object o) {
        return this == o;
    }

    @Override
    public int hashCode() {
        return System.identityHashCode(this);
    }
}

既然在使用现有 City class 时将在两个对象之间进行比较,您的测试就可以正常工作。但是在使用任何此类补救措施之前,您必须注意以下注意事项。

注意

正如 JEP-359 所说,记录更像是“数据载体”,在选择迁移现有 classes 时,您必须了解获得的 标准成员自动记录.

计划迁移一个必须了解当前实施的完整细节,例如在您按 City 分组时引用的示例中,不应该有两个城市相同的 idname data 以不同方式列出。它们应该相等,在所有重复 两次 之后应该是 相同的数据 ,因此计数正确。

在这种情况下,如果表示数据模型,您现有的实现可以通过覆盖 equals 实现来纠正以匹配 record 的方式,以考虑比较各个属性以及就是上面所说的直接补救措施是矛盾的,应该避免。