Контракт Equals и hashCode с EqualsVerifier

у меня есть некоторые сомнения о equals и hashCode контракт на Java с использованием EqualsVerifier библиотека.

представьте, что у нас есть что-то вроде этого

public abstract class Person {

    protected String name;

    @Override
    public boolean equals(Object obj) {
        // only name is taken into account
    }

    @Override
    public int hashCode() {
        // only name is taken into account
    }

}

и следующий расширенный класс:

public final class Worker extends Person {

    private String workDescription;

    @Override
    public final boolean equals(Object obj) {
        // name and workDescription are taken into account
    }

    @Override
    public final int hashCode() {
        // name and workDescription are taken into account
    }

}

Я пытаюсь проверить, выполняю ли я equals и hashCode контракт человек класс, используя EqualsVerifier

    @Test
    public void testEqualsAndHashCodeContract() {
        EqualsVerifier.forClass(Person.class).verify();
    }

запуск этого теста, я получаю, что я должен объявить equals и hashCode методы final, но это то, что я не хочу делать, потому что я могу объявить эти два метода в расширенных классах, так как я хочу использовать некоторые атрибуты ребенка в equals и hashCode.

не могли бы вы пропустить тестирование окончательного правила в библиотеке EqualsVerifier? Или я что-то упускаю?

2 ответов


отказ от ответственности: я создатель EqualsVerifier. Я только что обнаружил этот вопрос :).

обходной путь Иоахим Зауэр упоминает правильно.

позвольте мне объяснить, почему EqualsVerifier не нравится ваша реализация. Давайте пока притворимся, что Person не является абстрактным; это делает примеры немного проще. Допустим, у нас два Person объекты, вроде этого:

Person person1 = new Person("John");
Person person2 = new Worker("John", "CEO of the world");

и назовем equals на оба эти объекты:

boolean b1 = person1.equals(person2); // returns true
boolean b2 = person2.equals(person1); // returns false

b1 это правда, потому что Person ' s equals вызывается метод, и он игнорирует workDescription. b2 ложно, потому что Worker ' s equals вызывается метод, и instanceof или getClass() проверьте, что метод возвращает false.

другими словами, ваш equals метод больше не симметричен, и это требование для правильной реализации equals, по данным в Javadoc.

вы действительно можете использовать getClass() чтобы обойти эту проблему, но тогда вы столкнетесь с другой проблемой. Предположим, вы используете Hibernate или насмешливую структуру. Эти фреймворки используют манипуляции байт-кода для создания подклассов вашего класса. По сути, вы получите такой класс:

class Person$Proxy extends Person { }

Итак, предположим, вы совершаете туда и обратно в базу данных, например:

Person person1 = new Person("John");
em.persist(person1);
// ...
Person fetchedPerson = em.find(Person.class, "John");

а теперь давайте позвоним equals:

boolean b3 = person1.equals(fetchedPerson); // returns false
boolean b4 = fetchedPerson.equals(person1); // also returns false

b3 и b4 не соответствуют действительности, поскольку person1 и fetchedPerson относятся к разным классам (Person и Person$Proxy, если быть точным). equals теперь симметрично, поэтому, по крайней мере, он следует контракту, но это все еще не то, что вы хотите: fetchedPerson не "ведет себя" как


получить это право очень сложно.

документация EqualsVerifier объясняет обходной путь:

EqualsVerifier.forClass(MyClass.class)
    .withRedefinedSubclass(SomeSubclass.class)
    .verify();

обратите внимание, что для этого вам, вероятно, нужно проверить getClass() в равных потому что Worker может (или должен) никогда не быть равен Person.