Где в Java описан контракт метода equals(), включая правило транзитивности?

«Где в Java описан контракт метода equals(), включая правило транзитивности?» — вопрос из категории Other, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Правило транзитивности, как часть общего контракта для метода equals(), формально описано в Java Language Specification (JLS) и повторено в JavaDoc класса java.lang.Object.

Контракт equals() (из JLS §4.3.2 и JavaDoc): Для любых ненулевых ссылок x, y, z:

  1. x.equals(x)true (Рефлексивность)
  2. x.equals(y)y.equals(x) (Симметричность)
  3. Если x.equals(y) и y.equals(z), то x.equals(z) (Транзитивность)
  4. Многократные вызовы x.equals(y) возвращают одинаковый результат (Согласованность)
  5. x.equals(null)false

Проблема транзитивности в иерархиях наследования: Нарушение часто возникает при попытке добавить в подкласс новое значимое поле для сравнения.

class Point {
    private final int x, y;
    // equals сравнивает только x и y
}

class ColorPoint extends Point {
    private final Color color;

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) return false;
        // Нарушение! ColorPoint.equals(Point) может быть false,
        // но Point.equals(ColorPoint) будет true (если координаты совпадают).
        return super.equals(o) && ((ColorPoint) o).color == this.color;
    }
}
// Пример нарушения: 
// Point p = new Point(1,2);
// ColorPoint cp1 = new ColorPoint(1,2, RED);
// ColorPoint cp2 = new ColorPoint(1,2, BLUE);
// p.equals(cp1) == true, cp1.equals(cp2) == false, но p.equals(cp2) == true. Нет транзитивности.

Рекомендация: Избегать наследования для классов, добавляющих в equals новые поля. Использовать композицию.