Проблемы совместимости при преобразовании классов в записи - PullRequest
6 голосов
/ 22 марта 2020

Я работал со следующим классом с именем 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 ключа и каждый из которых приходится на 1 его вхождение. С представлением записи в результирующем Map не более 3 ключей. Что является причиной этого и каким должен быть путь к go вокруг этого?

1 Ответ

5 голосов
/ 22 марта 2020

Причина

Причины наблюдаемого поведения описаны в 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", подсчитанный дважды.

Немедленное исправление

An немедленным временным решением этой проблемы было бы создание собственной (ошибочной - причина объяснена позже) 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, не должно быть причин иметь два города с одинаковыми id и name data должны быть перечислены по-разному. Они должны быть равны, это должны быть одинаковые данные после всех повторений дважды и, следовательно, правильные значения.

В этом случае ваша существующая реализация, если представляет данные модель может быть исправлена ​​таким образом, чтобы она соответствовала record, путем перезаписи реализации equals, чтобы учесть сравнение отдельных атрибутов, в которых непосредственное средство защиты, указанное выше, противоречиво и его следует избегать.

...