5 контрактов java equals

Equals в переводе с английского - равный. Для определения равенства двух объектов сравнивается физический адрес в памяти. Контейнеры, такие как java.util.Set, проверяют схожесть добавляемого объекта с уже имеющимеся. При совпадении старая версия будет заменена новой.

Сравнение доменных объектов по их физическому адресу может вызывать интересный эффект: два абсолютно одинаковых объекта оказываются не равны.

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

import java.lang.String;

public class Main {

    private static class Point {
        private int x = 0, y = 0;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public static void main(String[] args) {
        Point source = new Point(10, 10);
        Point target = new Point(10, 10);
        System.out.println("Is point equals ? " + source.equals(target));
    }
}

Программа выведет на консоль сообщение:

Is point equals ? false

Решение проблемы - сравнивать содержимое объекта. Переопределённый метод equals должен выполнять 5 правил (контрактов):

1. Рефлексивность

Для каждого экземпляра x должно выполнятся условие:

assert x.equals(x) == true;
2. Симметричность

Для каждого экземпляра x и y x.equals(y) должен возвращать true только тогда, когда y.equals(x) возвращает true:

if(x.equals(y))
 assert y.equals(x) == true;
3. Переносимость

Для каждого экземпляра x, y и z должно выполнятся условие: если x.equals(y) возвращает true и y.equals(z) возращает true, тогда x.equals(z) должно возращать true

if(x.equals(y) && y.equals(z))
  assert x.equals(z) == true;
4. Консистентность

Для каждого экземпляра x и y повторное выполнение x.equals(y) должно возвращать одинаковый результат:

if(x.equals(y))
    assert x.equals(y) == true;
5. Сравнение с null

Для каждого экземпляра x x.equals(null) должно возвращать false:

assert x.equals(null) == false;

Переопределение equals

Исправим исходный код программы в соответсвии с контрактами:

import java.lang.String;

public class Main {

    private static class Point {
        private int x = 0, y = 0;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null || !(obj instanceof Point))
                return false;

            Point that = (Point) obj;
            if (this.x != that.x)
                return false;
            return this.y == that.y;
        }
    }

    public static void main(String[] args) {
        Point source = new Point(10, 10);
        Point target = new Point(10, 10);
        System.out.println("Is point equals ? " + source.equals(target));
    }
}

и запустим ещё раз:

Is point equals ? true

С помощью переопределения equals шанс появления фантомных объектов исключён.

Переопределяя equals не забывайте о hashCode

Переопределение equals всегда сопровождается изменением hashCode. Некоторые контейнеры, такие как java.util.Set, перед проверкой схожести объектов сверяют их хэш.

Переопределение hashCode также сопровождается рядом правил (контрактов):

1. Консистентность

Для каждого экземпляра x повторное выполнение x.hashCode() должно возвращать одинаковый хэш:

int hash = x.hashCode();
assert hash == x.hashCode();

2. Схожие объекты имеют одинаковый хэш

Если два экземпляра схожи друг с другом, то вызов hashCode() у каждого из них должен возвращать одинаковый результат:

if(x.equals(y))
  assert x.hashCode() == y.hashCode();

Однако у разных объектов хэш может не совпадать.

Переопределение hashCode

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

import java.lang.String;

public class Main {

    private static class Point {
        private int x = 0, y = 0;

        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null || !(obj instanceof Point))
                return false;

            Point that = (Point) obj;
            if (this.x != that.x)
                return false;
            return this.y == that.y;
        }

        @Override
        public int hashCode() {
            int result = x;
            result = 31 * result + y;
            return result;
        }
    }

    public static void main(String[] args) {
        Point source = new Point(10, 10);
        Point target = new Point(10, 10);
        System.out.println("Is point equals ? " + source.equals(target));
        System.out.println("Is hashCode equals ? " + (source.hashCode() == target.hashCode()));
    }
}

запуск программы выведет на консоль:

Is point equals ? true
Is hashCode equals ? true

Выводы

Переопределение equals нельзя делать на скорую руку. Несоблюдение одного из контрактов неочевидны и могут повлечь серъезные последствия.

Например, расширяя класс Point в ColorPoint нарушается контракт симметричности. Поэтому в методе Point.equals проверку instanceof следует заменить на obj.getClass().equals(Point.class).

С другой стороны проверяя схожесть объектов необходимость в поиске фантомов снижается. Исходный код становится более читаемым.