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 правил (контрактов):
Для каждого экземпляра x должно выполнятся условие:
assert x.equals(x) == true;
Для каждого экземпляра x и y x.equals(y) должен возвращать true только тогда, когда y.equals(x) возвращает true:
if(x.equals(y))
assert y.equals(x) == true;
Для каждого экземпляра 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;
Для каждого экземпляра x и y повторное выполнение x.equals(y) должно возвращать одинаковый результат:
if(x.equals(y))
assert x.equals(y) == true;
Для каждого экземпляра x x.equals(null) должно возвращать false:
assert x.equals(null) == false;
Исправим исходный код программы в соответсвии с контрактами:
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. Некоторые контейнеры, такие как java.util.Set, перед проверкой схожести объектов сверяют их хэш.
Переопределение hashCode также сопровождается рядом правил (контрактов):
Для каждого экземпляра x повторное выполнение x.hashCode() должно возвращать одинаковый хэш:
int hash = x.hashCode();
assert hash == x.hashCode();
Если два экземпляра схожи друг с другом, то вызов hashCode() у каждого из них должен возвращать одинаковый результат:
if(x.equals(y))
assert x.hashCode() == y.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).
С другой стороны проверяя схожесть объектов необходимость в поиске фантомов снижается. Исходный код становится более читаемым.