Почему я не должен использовать равные с наследованием?

Когда я читал книгу Java, автор сказал, что при разработке класса обычно небезопасно использовать equals() с наследованием. Например:

public final class Date {
    public boolean equals(Object o) {
         // some code here
    }
}

В вышеприведенном классе мы должны положить final, поэтому другой класс не может наследовать от этого. И мой вопрос в том, почему это небезопасно, когда разрешить другому классу наследовать от этого?

Ответ 1

Потому что трудно (невозможно?) Сделать это правильно, особенно симметричное свойство.

Скажем, у вас есть Vehicle класса, а Car extends Vehicle класса Car extends Vehicle. Vehicle.equals() true если аргумент также является Vehicle и имеет тот же вес. Если вы хотите реализовать Car.equals() он должен давать значение true только если аргумент также является автомобилем, и, кроме веса, он также должен сравнивать make, engine и т.д.

Теперь представьте следующий код:

Vehicle tank = new Vehicle();
Vehicle bus = new Car();
tank.equals(bus);  //can be true
bus.equals(tank);  //false

Первое сравнение может дать true если по совпадению танк и автобус имеют одинаковый вес. Но так как танк - это не машина, сравнение его с машиной всегда приводит к false.

У вас есть несколько обходных путей:

  • строгий: два объекта равны тогда и только тогда, когда они имеют одинаковый тип (и все свойства равны). Это плохо, например, когда вы создаете подкласс, чтобы добавить немного поведения или украсить исходный класс. Некоторые фреймворки также создают подклассы для ваших классов без вашего ведома (Hibernate, Spring AOP с прокси CGLIB...)

  • свободный: два объекта равны, если их типы "совместимы" и имеют одинаковое содержимое (семантически). Например, два набора равны, если они содержат одинаковые элементы, не имеет значения, что один является HashSet а другой TreeSet (спасибо @veer за указание на это).

    Это может вводить в заблуждение. Возьмите два LinkedHashSet (где порядок вставки имеет значение как часть контракта). Однако, поскольку equals() учитывает только необработанный контракт Set, сравнение дает значение true даже для явно разных объектов:

    Set<Integer> s1 = new LinkedHashSet<Integer>(Arrays.asList(1, 2, 3));
    Set<Integer> s2 = new LinkedHashSet<Integer>(Arrays.asList(3, 2, 1));
    System.out.println(s1.equals(s2));
    

Ответ 2

Мартин Одерски (парень по генерики в Java и исходная кодовая база для текущего javac) имеет приятную главу в своей книге "Программирование в canEqual может исправить проблему равенства/наследования. Вы можете прочитать обсуждение в первом издании своей книги, которое доступно в Интернете:

Глава 28 программирования в Scala, первое издание: Равенство объектов

Книга, конечно, относится к Scala, но те же идеи относятся к классической Java. Исходный код образца не должен быть слишком сложным для того, чтобы кто-то из фона Java мог понять.

Edit:

Похоже, Odersky опубликовал статью о той же концепции в Java еще в 2009 году, и она доступна на том же сайте:

Как написать метод равенства в Java

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