Ответ Килиана Фота превосходен. Я просто хотел бы добавить канонический пример * того, почему это проблема. Представьте себе целочисленный класс Point:
class Point2D {
public int x;
public int y;
// constructor
public Point2D(int theX, int theY) { x = theX; y = theY; }
public int hashCode() { return x + y; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point2D) ) { return false; }
Point2D that = (Point2D) o;
return (x == that.x) &&
(y == that.y);
}
}
Теперь давайте подкласс это, чтобы быть 3D-точкой.
class Point3D extends Point2D {
public int z;
// constructor
public Point3D(int theX, int theY, int theZ) {
super(x, y); z = theZ;
}
public int hashCode() { return super.hashCode() + z; }
public boolean equals(Object o) {
if (this == o) { return true; }
if ( !(o instanceof Point3D) ) { return false; }
Point3D that = (Point3D) o;
return super.equals(that) &&
(z == that.z);
}
}
Супер просто! Давайте использовать наши очки:
Point2D p2a = new Point2D(3, 5);
Point2D p2b = new Point2D(3, 5);
Point2D p2c = new Point2D(3, 7);
p2a.equals(p2b); // true
p2b.equals(p2a); // true
p2a.equals(p2c); // false
Point3D p3a = new Point3D(3, 5, 7);
Point3D p3b = new Point3D(3, 5, 7);
Point3D p3c = new Point3D(3, 7, 11);
p3a.equals(p3b); // true
p3b.equals(p3a); // true
p3a.equals(p3c); // false
Вы, наверное, удивляетесь, почему я публикую такой простой пример. Вот подвох:
p2a.equals(p3a); // true
p3a.equals(p2a); // FALSE!
Когда мы сравниваем 2D-точку с эквивалентной 3D-точкой, мы получаем истинное значение, но когда мы обращаемся к обратному сравнению, мы получаем ложное значение (потому что p2a не выполняется instanceof Point3D
)
Вывод
Обычно возможно реализовать метод в подклассе таким образом, что он больше не совместим с тем, как суперкласс ожидает его работы.
Как правило, невозможно реализовать equals () для существенно другого подкласса способом, совместимым с его родительским классом.
Когда вы пишете класс, который вы хотите разрешить людям создавать подклассы, очень полезно написать контракт о том, как должен вести себя каждый метод. Еще лучше был бы набор модульных тестов, которые люди могли бы запускать против своих реализаций переопределенных методов, чтобы доказать, что они не нарушают контракт. Почти никто не делает этого, потому что это слишком много работы. Но если вам все равно, это то, что нужно сделать.
Отличным примером хорошо прописанного контракта является Comparator . Просто игнорируйте то, о чем говорится, .equals()
по причинам, описанным выше. Вот пример того, как Comparator может делать то, что .equals()
нельзя .
Заметки
Источником этого примера был пункт 8 «Эффективная Java» Джоша Блоха, но Блох использует ColorPoint, который добавляет цвет вместо третьей оси и использует двойные вместо целых. Пример Java Блоха в основном продублирован Odersky / Spoon / Venners, которые сделали свой пример доступным в Интернете.
Несколько человек возражали против этого примера, потому что, если вы сообщите родительскому классу о подклассе, вы можете исправить эту проблему. Это верно, если имеется достаточно небольшое количество подклассов и если родитель знает обо всех них. Но первоначальный вопрос был о создании API, для которого кто-то еще напишет подклассы. В этом случае вы обычно не можете обновить родительскую реализацию, чтобы она была совместима с подклассами.
бонус
Компаратор также интересен тем, что он решает проблему правильной реализации equals (). Более того, он следует шаблону для решения этого типа проблемы наследования: шаблону разработки стратегии. Классы типов, от которых люди в Haskell и Scala приходят в восторг, также являются образцом стратегии. Наследование не плохо или неправильно, это просто сложно. Для дальнейшего чтения посмотрите статью Филиппа Уодлера « Как сделать специальный полиморфизм менее специальным»