JavaRush /Курсы /JAVA 25 SELF /Контракты equals и hashCode

Контракты equals и hashCode

JAVA 25 SELF
29 уровень , 0 лекция
Открыта

1. Введение

Как мы сравниваем объекты

В Java объекты — это не просто данные, у каждого есть собственный адрес в памяти. Оператор == отвечает на вопрос «это одна и та же коробочка?», то есть сравнивает ссылки (адреса), а не содержимое. Метод equals предназначен для сравнения именно содержимого. По умолчанию, если его не переопределить, equals ведёт себя как ==.

Person p1 = new Person("Иван", 20);
Person p2 = new Person("Иван", 20);

System.out.println(p1 == p2); // false — это разные объекты в памяти!

Чтобы два разных объекта считались равными по данным (например, совпадают все значимые поля), необходимо переопределить equals. И если вы планируете использовать объекты в хеш-коллекциях, обязательно вместе с ним корректно переопределяйте и hashCode.

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // это один и тот же объект
        if (o == null || getClass() != o.getClass()) return false; // проверяем класс
        Person person = (Person) o; // приводим к нужному типу
        return age == person.age && name.equals(person.name); // сравниваем поля
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // чтобы работало с HashSet/HashMap
    }
}

Такое переопределение критично для корректной работы с HashSet/HashMap: без equals и hashCode коллекции будут считать даже одинаковые по данным объекты разными.

Отношение эквивалентности в equals зависит от ваших требований: можно сравнивать по всем полям, по части полей (например, только email у User) — важна консистентность и соблюдение контракта.

Где это особенно важно?

  • В коллекциях на базе хеш-таблиц: HashSet, HashMap, LinkedHashSet и др.
  • При поиске и удалении элементов в коллекциях: без корректного equals нужный объект может «не находиться».
  • В бизнес-логике: например, два User с одинаковым email должны считаться одним пользователем.

hashCode — зачем он нужен?

Хеш-коллекции (например, HashSet, HashMap) используют хеш-таблицы. Метод hashCode вычисляет целое число — «адрес корзины», куда попадёт объект. Если два объекта равны по equals, их hashCode обязан совпадать. Нарушите это правило — и коллекции начнут вести себя непредсказуемо.

2. Контракт equals и hashCode

Контракт equals

Основные требования к поведению equals:

  • Рефлексивность: a.equals(a) всегда true.
  • Симметричность: если a.equals(b) — true, то и b.equals(a) — true.
  • Транзитивность: если a.equals(b) и b.equals(c), то a.equals(c) — тоже true.
  • Согласованность: при неизменности объектов результат вызовов стабилен.
  • Сравнение с null: любой объект не равен null.

Контракт hashCode

  • Если два объекта равны по equals, их hashCode равны.
  • Если объекты не равны, их хеш-коды могут совпадать (коллизии допустимы, но нежелательны).
  • Пока объект не меняется логически, его hashCode должен оставаться постоянным.

Иными словами, совпадающий hashCode — необходимое, но не достаточное условие равенства: одинаковый хеш не гарантирует равенство по equals.

3. Реализация equals и hashCode: пример

Рассмотрим класс Person, где равенство определяется по полям name и age.

public class Person {
    private String name;
    private int age;

    // Конструктор, геттеры, сеттеры...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // Сравнение ссылок
        if (o == null || getClass() != o.getClass()) return false; // Проверка класса

        Person person = (Person) o; // Приведение типа

        // Сравниваем поля
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 — распространённый выбор простого числа
        return result;
    }
}
  • Сначала быстрые проверки: ссылка и класс.
  • Затем сравнение значимых полей.
  • В hashCode используется простое число 31 для уменьшения коллизий.

Использование Objects.equals и Objects.hash

Начиная с Java 7, класс Objects упрощает код и делает его безопаснее к null:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals, hashCode и compareTo: как они связаны

Как связаны equals и compareTo?

Интерфейс Comparable задаёт метод compareTo, который возвращает отрицательное/нулевое/положительное число для «меньше/равно/больше». Желательно, чтобы из a.compareTo(b) == 0 следовало a.equals(b). Обратное не обязательно.

Если нарушить согласованность (например, compareTo сравнивает только по возрасту, а equals — по имени и возрасту), то сортируемые коллекции вроде TreeSet/TreeMap могут вести себя неожиданно: объекты считаются «равными» с точки зрения порядка, но не равными по содержимому.

equals и hashCode в коллекциях

  • В HashSet и HashMap операции добавления/поиска/удаления полагаются на корректную реализацию equals и hashCode.
  • Без переопределения эти коллекции считают объекты «разными», даже если их данные одинаковы.

5. Примеры: как это работает в коллекциях

HashSet: хранение уникальных объектов

Set<Person> people = new HashSet<>();
people.add(new Person("Иван", 20));
people.add(new Person("Иван", 20)); // Дубликат

System.out.println(people.size()); // 1, если equals/hashCode реализованы правильно

Без корректного equals/hashCode в набор попадут оба объекта.

HashMap: поиск по ключу

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Анна", 25);
Person p2 = new Person("Анна", 25);

map.put(p1, "Пользователь 1");
System.out.println(map.get(p2)); // "Пользователь 1", если equals/hashCode реализованы правильно

Без контракта коллекция вернёт null — для неё это «разные» ключи.

6. Best practices: советы по реализации

  • Включайте в equals/hashCode все поля, определяющие «идентичность» объекта.
  • Не используйте изменяемые поля (которые меняются после добавления в коллекции) при расчёте hashCode.
  • Поручайте генерацию методов IDE — меньше шансов на опечатки.
  • В equals сначала проверяйте this == o, затем класс, затем поля.
  • Для сравнения полей-объектов используйте Objects.equals.
  • Для хеш-кода используйте Objects.hash или проверенный шаблон с множителем 31.

7. Полезные нюансы

Почему нельзя использовать только hashCode?

Коллизии неизбежны: разные объекты могут иметь одинаковый hashCode. Хеш — лишь быстрый ориентир для корзины; окончательное решение о равенстве принимает equals.

Можно ли не переопределять equals и hashCode?

Только если вы уверены, что объекты никогда не будут сравниваться по содержимому и не станут ключами/уникальными элементами в коллекциях. На практике это редкость.

Различие между ==, equals и compareTo

Оператор/метод Что сравнивает? Для чего нужен?
==
Ссылки (адреса в памяти) Проверка «один и тот же объект?»
equals
Содержимое объектов Равенство по бизнес-логике
compareTo
Порядок (меньше/равно/больше) Сортировка, упорядочивание

8. Типичные ошибки при реализации equals и hashCode

Ошибка №1: Переопределили equals, но забыли hashCode. Объекты считаются равными, но попадают в разные корзины хеш-таблицы — поиск и удаление ломаются.

Ошибка №2: Используете изменяемые поля в hashCode. Если поле изменится после добавления в коллекцию, объект «потеряется»: хеш изменится, а корзина — нет.

Ошибка №3: Нарушена симметричность/транзитивность equals. a.equals(b) даёт true, а b.equals(a) — false, или ломается транзитивность — коллекции начинают вести себя непредсказуемо.

Ошибка №4: Не проверяете класс в equals. Сравнение объектов разных классов приводит к неверным результатам или исключениям.

Ошибка №5: Сравниваете строки и объекты через ==. Оператор == сравнивает ссылки; используйте equals для содержимого.

Ошибка №6: Нет согласованности compareTo и equals. Если a.compareTo(b) == 0, но !a.equals(b), коллекции TreeSet/TreeMap могут считать элементы равными для порядка, но разными по равенству — это источник «призраков» и дубликатов.

1
Задача
JAVA 25 SELF, 29 уровень, 0 лекция
Недоступна
Учёт уникальных городов в виртуальном мире 🗺️
Учёт уникальных городов в виртуальном мире 🗺️
1
Задача
JAVA 25 SELF, 29 уровень, 0 лекция
Недоступна
Управление сотрудниками в новой HR-системе 👥
Управление сотрудниками в новой HR-системе 👥
Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Victor Уровень 37
16 августа 2026
16.08.2026
Anna Klushkina Уровень 66
20 июля 2026
29
Gans Electro Уровень 40
18 июля 2026
Не смог понять:

Желательно, чтобы из a.compareTo(b) == 0 следовало a.equals(b).
Обратное не обязательно.
Если так?

new BigDecimal("1.0")
new BigDecimal("1.00")

//compareTo() == 0
//equals() == false
24 июня 2026
Тема `equals()` / `hashCode()` раскрыта не полностью. Например, было бы полезно отдельно разобрать, что произойдёт, если переопределённый метод `hashCode()` всегда будет возвращать одно и то же значение, например `1`. Как в таком случае будет работать `HashMap`? Будет ли она работать корректно вообще? Что произойдёт с производительностью и почему? Такие вопросы часто задают на собеседованиях, поэтому, на мой взгляд, эту тему стоило бы объяснить подробнее.
Hipsta Krippo Уровень 30
18 декабря 2025
Хоть вы не пройдете тесты к задачам, тем не менее хочу заметить: обе задачи можно решить в одно строчку - с помощью применения record-классов;)
Big198801 Уровень 35
23 января 2026
все же для успешного решения второй задачи по ТЗ мне пришлось переопределять equals(), т.к. по умолчанию record класс проверял только по полю employeeId )))
Andrey Уровень 6
26 сентября 2025
29