Контракт equals/hashCode
Почему они должны согласовываться
«Контракт equals/hashCode» — бесплатный урок Java Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Java Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Java Academy содержит 4 уроков всего.
Два метода, один контракт
Каждый объект Java наследует equals() и hashCode() от Object.
Если Вы переопределяете один из них, почти всегда необходимо переопределить и второй. Вместе они образуют обязательный контракт, на который опираются коллекции, использующие хеширование.
public class Main {
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}Основное правило
Контракт устанавливает следующие правила:
- Если
a.equals(b)имеет значение истина, тоa.hashCode()должен быть равенb.hashCode(). - Если хеш-коды различаются, объекты гарантированно не равны.
Равные объекты должны иметь одинаковые хеш-коды. Обратное не требуется.
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hel" + "lo";
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() == s2.hashCode());
}
}Неравные объекты могут иметь общий хеш-код
Два неравных объекта могут иметь один и тот же хеш-код. Это всего лишь коллизия, и map обрабатывает её с помощью equals().
Таким образом, запрещена только одна ситуация: равные объекты имеют разные хеш-коды.
public class Main {
public static void main(String[] args) {
// Equal value, unequal objects, same hash is fine
System.out.println("FB".hashCode() == "Ea".hashCode());
System.out.println("FB".equals("Ea"));
}
}Класс, переопределяющий только equals
Ниже приведён сломанный класс. Он переопределяет equals(), но не переопределяет hashCode().
Теперь две логически равные точки получают разные хеш-коды, унаследованные от Object, что нарушает контракт.
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
// BUG: no hashCode override
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println("equals: " + a.equals(b));
System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
}
}Ошибка в HashSet
Сломанный класс неправильно работает в HashSet. Множество ищет элемент в корзине, выбранной по неправильному хеш-коду, и не находит равный элемент.
В результате появляются дубликаты, которые должны были быть отклонены.
import java.util.HashSet;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}Исправление
Переопределите оба метода и вычисляйте их на основе одних и тех же полей. Тогда равные объекты будут иметь общий хеш-код и попадут в одну корзину.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
static class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}equals должен быть рефлексивным
Корректный equals() обладает четырьмя свойствами. Первое свойство — рефлексивность: x.equals(x) должно возвращать истину.
Это кажется очевидным, но пользовательская логика может нарушить данное свойство, если Вы забудете обработать случай сравнения ссылки с самой собой.
public class Main {
public static void main(String[] args) {
String x = "java";
System.out.println(x.equals(x));
}
}Симметричность и транзитивность
Ещё два правила:
- Симметричность: если
a.equals(b), тоb.equals(a). - Транзитивность: если
a.equals(b)иb.equals(c), тоa.equals(c).
Смешивание типов в иерархии классов часто нарушает симметричность, поэтому тщательно сравнивайте классы.
public class Main {
public static void main(String[] args) {
String a = "x", b = "x", c = "x";
boolean symmetric = a.equals(b) == b.equals(a);
boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
System.out.println("symmetric: " + symmetric);
System.out.println("transitive: " + transitive);
}
}Согласованность
Четвёртое правило — согласованность: повторные вызовы возвращают один и тот же результат, пока объекты не изменяются.
Поэтому следует строить equals и hashCode на основе неизменяемых полей. Изменение поля, использованного в hashCode, после добавления объекта повреждает map.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
// Immutable record gives consistent equals/hashCode automatically
record Id(int value) {}
Id id = new Id(7);
System.out.println(Objects.equals(id, new Id(7)));
System.out.println(id.hashCode() == new Id(7).hashCode());
}
}Записи делают это за Вас
Java record автоматически создаёт equals() и hashCode() на основе всех своих компонентов.
Для данных, подобных значениям, предпочитайте записи. Контракт выполняется автоматически и остаётся согласованным.
import java.util.HashSet;
import java.util.Set;
public class Main {
record Point(int x, int y) {}
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
System.out.println("size = " + set.size());
}
}Ловушка изменяемого ключа
Если после добавления объекта в map изменить поле, использованное в hashCode, найти этот объект больше нельзя.
map ищет в старой корзине, а теперь объект хешируется в новую. Запись становится потерянным призраком.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Main {
static class Box {
int id;
Box(int id) { this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof Box b && b.id == id;
}
@Override public int hashCode() { return Objects.hash(id); }
}
public static void main(String[] args) {
Map<Box, String> m = new HashMap<>();
Box key = new Box(1);
m.put(key, "value");
key.id = 99; // mutated after insertion
System.out.println(m.get(key));
}
}Быстрая проверка
Проверьте, насколько хорошо Вы усвоили этот контракт.
Итоги
Вы изучили контракт equals/hashCode:
- Равные объекты должны иметь одинаковые хеш-коды.
- equals должен быть рефлексивным, симметричным, транзитивным и согласованным.
- Оба метода следует основывать на одних и тех же неизменяемых полях.
- Записи автоматически создают корректные реализации.
Далее Вы вручную напишете собственный корректный hashCode.
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
}
}Часто задаваемые вопросы
Урок «Контракт equals/hashCode» бесплатный?
Да — полный текст урока «Контракт equals/hashCode» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Java Academy, подпишись на CoddyKit PRO. Курс Java Academy содержит 4 уроков всего.
Чему я научусь в уроке «Контракт equals/hashCode»?
Почему они должны согласовываться Ты практикуешь Java Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Java Academy?
Предыдущий опыт не требуется. Java Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Контракт equals/hashCode»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Java Academy?
Да. Каждый урок Java Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Как работает HashMap
- Контракт equals/hashCode
- Реализация hashCode
- Преобразование в дерево и производительность