11

我一直在使用以下名为City

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

并试图将其转换为被record称为CityRecord

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 个。使用记录表示,结果中的键不超过 3 个Map。是什么导致了这种情况,应该采取什么方法来解决这个问题?

4

1 回答 1

10

原因

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

对于所有记录类,以下不变量必须保持:如果记录 R 的组件是 c1、c2、...cn,那么如果记录实例被复制如下:

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

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

因此,推断/断言应该有三个这样的键,其中一个已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类时一样,您的测试就可以正常工作了。但在使用任何此类补救措施之前,您必须注意以下注意事项。

警告

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

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

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

于 2020-03-22T07:40:30.790 回答