将 类 转换为记录时出现兼容性问题
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
分组时引用的示例中,不应该有两个城市相同的 id
和 name
data 以不同方式列出。它们应该相等,在所有重复 两次 之后应该是 相同的数据 ,因此计数正确。
在这种情况下,如果表示数据模型,您现有的实现可以通过覆盖 equals
实现来纠正以匹配 record
的方式,以考虑比较各个属性以及就是上面所说的直接补救措施是矛盾的,应该避免。
我一直在与以下名为 City
@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
分组时引用的示例中,不应该有两个城市相同的 id
和 name
data 以不同方式列出。它们应该相等,在所有重复 两次 之后应该是 相同的数据 ,因此计数正确。
在这种情况下,如果表示数据模型,您现有的实现可以通过覆盖 equals
实现来纠正以匹配 record
的方式,以考虑比较各个属性以及就是上面所说的直接补救措施是矛盾的,应该避免。