Відповідь Кіліана Фота відмінна. Я просто хотів би додати канонічний приклад * того, чому це проблема. Уявіть цілий клас точок:
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);
}
}
Тепер давайте підклас це буде тривимірною точкою.
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()
з причин, описаних вище. Ось приклад того, як компаратор може робити речі, .equals()
не може .
Примітки
Пункт 8 "Ефективна Java" Джоша Блоха був джерелом цього прикладу, але Bloch використовує ColorPoint, який додає колір замість третьої осі та використовує подвійні замість ints. Приклад Java Блоха в основному дублюється Одерським / Ложкою / Веннери, які зробили свій приклад доступним в Інтернеті.
Кілька людей заперечували проти цього прикладу, тому що якщо ви повідомлите батьківський клас про підклас, ви можете виправити цю проблему. Це вірно, якщо є достатньо мала кількість підкласів і якщо батько знає про них усіх. Але початкове питання полягало у створенні API, для якого хтось інший буде писати підкласи. У такому випадку ви, як правило, не можете оновити батьківську реалізацію на сумісність з підкласами.
Бонус
Компаратор цікавий також тим, що він вирішує питання про те, як правильно реалізувати рівний (). А ще краще - це слід за шаблоном виправлення такого типу питання спадкування: модель розробки стратегії. Типи класів, якими хвилюються люди Хаскелл та Скала, - це також стратегія. Спадщина не є поганим чи неправильним, це просто хитро. Для подальшого читання ознайомтеся з документом Філіпа Вадлера Як зробити спеціальний поліморфізм менш спеціальним