Проблемы совместимости при преобразовании классов в записи
Я работал со следующим классом с именем 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()));
В приведенном выше коде утверждено, что он содержит 4 ключа, каждый из которых соответствует одному вхождению. При представлении записи в результирующем файле не более 3 ключей.Map
. Что вызывает это и как это можно обойти?
1 ответ
Причина
Причина наблюдаемого поведения описана в 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
class, ваши тесты будут работать нормально. Но вы должны принять во внимание приведенную ниже осторожность, прежде чем использовать любое такое средство.
Осторожно
Как гласит JEP-359, записи больше похожи на "носитель данных", и, выбирая перенос существующих классов, вы должны знать о стандартных членах, автоматически получаемых записью.
Планируя миграцию, необходимо знать все детали текущей реализации, например, в приведенном вами примере, когда вы сгруппировали City
, не должно быть причин иметь два города с одинаковыми id
а также name
данные должны быть указаны иначе. Они должны быть равны, это должны быть одни и те же данные после того, как все повторяются дважды, а значит, и правильные подсчеты.
В этом случае ваша существующая реализация, представляющая модель данных, может быть исправлена, чтобы соответствовать record
в некотором смысле путем перезаписи equals
реализация, чтобы учесть сравнение отдельных атрибутов, где немедленное решение, указанное выше, является противоречивым, и его следует избегать.