При переопределении метода equals()
в Java/Kotlin необходимо строго соблюдать несколько фундаментальных правил (контракт), определенных в документации Java. Нарушение этих правил может привести к непредсказуемому поведению коллекций и других структур данных.
x.equals(x) должен возвращать true для любого ненулевого x
Пример нарушения:
@Override
public boolean equals(Object o) {
return random.nextBoolean(); // Нарушает рефлексивность
}
x.equals(y) должен возвращать тот же результат, что и y.equals(x)
Пример проблемы:
class Color {
String name;
// equals сравнивает только по name
}
class RGBColor extends Color {
int rgb;
// equals сравнивает name + rgb
}
В этом случае color.equals(rgbColor)
и rgbColor.equals(color)
дадут разные результаты.
Если x.equals(y) и y.equals(z), то x.equals(z)
Пример нарушения:
class Point {
int x, y;
// equals сравнивает только x
}
class ColoredPoint extends Point {
Color color;
// equals сравнивает x + color
}
Может возникнуть ситуация, где pointA.equals(coloredPointB)
и coloredPointB.equals(pointC)
, но pointA.equals(pointC)
= false.
Многократные вызовы x.equals(y) должны стабильно возвращать одно и то же значение
Плохая практика:
@Override
public boolean equals(Object o) {
return System.currentTimeMillis() % 2 == 0; // Результат меняется со временем
}
x.equals(null) всегда должен возвращать false
Правильная проверка:
@Override
public boolean equals(Object o) {
if (o == null) return false;
// или более правильный вариант:
// if (o == null || getClass() != o.getClass()) return false;
// ...
}
Если x.equals(y) == true, то x.hashCode() == y.hashCode()
Но обратное не обязательно верно.
Метод должен обрабатывать все входные данные, а не бросать исключения при неверных типах.
Для Java:
@Override
public boolean equals(Object o) { // Обязательно Object, не ваш класс!
// реализация
}
Для Kotlin:
override fun equals(other: Any?): Boolean { // Any?, а не ваш тип!
// реализация
}
Пример для Java:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyClass that = (MyClass) o;
return Objects.equals(field1, that.field1) &&
Objects.equals(field2, that.field2);
}
Пример для Kotlin:
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MyClass
return field1 == other.field1 &&
field2 == other.field2
}
Сравнение изменяемых полей:
@Override
public boolean equals(Object o) {
// Поле может измениться после добавления в HashSet
return Objects.equals(this.timestamp, ((Event)o).timestamp);
}
Нарушение LSP (принципа подстановки Барбары Лисков):
@Override
public boolean equals(Object o) {
if (!(o instanceof MyClass)) return false; // Нарушение для подклассов
// ...
}
Игнорирование переопределения hashCode():
// Переопределили equals(), но забыли hashCode()
// Приведет к проблемам в HashMap/HashSet
контракт equals() включает рефлексивность, симметричность, транзитивность, консистентность и корректную обработку null. Всегда переопределяйте hashCode() вместе с equals(), избегайте сравнения изменяемых полей и следите за поведением в иерархиях наследования.