Каков контракт между методами equals() и hashCode() в Java?

Ответ

Контракт (соглашение), определенный в документации Java, гласит:

  1. Согласованность во времени: Если equals() для двух объектов возвращает true, то вызов hashCode() для каждого из них должен возвращать одно и то же целочисленное значение. При повторных вызовах на неизмененном объекте hashCode() должен возвращать то же значение.
  2. Обратное не обязательно: Если hashCode() для двух объектов совпадает, это не гарантирует, что equals() вернет true (это коллизия хэша).

Зачем это нужно? Этот контракт критически важен для корректной работы hash-базированных коллекций: HashMap, HashSet, Hashtable. Они используют хэш-код для быстрого определения «корзины» (bucket), в которой может находиться объект.

Нарушение контракта приводит к ошибкам:

class BrokenPerson {
    private String name;
    private int age;

    public BrokenPerson(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;
        BrokenPerson that = (BrokenPerson) o;
        return age == that.age && Objects.equals(name, that.name);
    }
    // НЕТ переопределения hashCode()! Используется реализация Object.hashCode()
}

// Проблема в использовании:
Set<BrokenPerson> set = new HashSet<>();
BrokenPerson p1 = new BrokenPerson("Alice", 30);
BrokenPerson p2 = new BrokenPerson("Alice", 30);

System.out.println(p1.equals(p2)); // true (логически равны)
set.add(p1);
System.out.println(set.contains(p2)); // Может вернуть FALSE! Объекты попали в разные корзины.

Правильная реализация: Всегда переопределяйте hashCode(), если переопределяете equals(). Используйте одни и те же значимые поля в обоих методах. Удобно делать это с помощью Objects.hash().

class CorrectPerson {
    private String name;
    private int age;

    public CorrectPerson(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;
        CorrectPerson that = (CorrectPerson) o;
        return age == that.age && Objects.equals(name, that.name); // Поля для equals
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Те же поля, что и в equals()!
    }
}

Ответ 18+ 🔞

А, слушай, смотри, вот эта вся хуйня с equals и hashCode в Java — это, блядь, не просто так придумали, это прям священный договор, контракт, который нарушать — себе дороже, в рот меня чих-пых!

Представь себе, сидит где-то в недрах Oracle какой-то ёбаный мудозвон и пишет в документации: «Ребята, договорились так, окей?». И договор вот какой:

  1. Согласованность, блядь. Если два объекта, по твоему мнению, равны (то есть equals() говорит «да, сука, это одно и то же»), то и хэш-код у них обязан быть одинаковый. Один и тот же, блядь, цифровой отпечаток. И не меняться, пока объект не поменяли.
  2. А вот наоборот — хуй там. Если хэш-коды совпали — это нихуя не значит, что объекты равны. Это просто коллизия, совпадение, типа как два разных человека могут жить в одном подъезде. Бывает, епта.

А нахуя это всё? А нахуя? Да затем, что все эти твои HashMap, HashSet — они же не тупые, они хотят работать быстро. Они смотрят на хэш-код, чтобы понять, в какую «корзину» (bucket, блядь) запихнуть объект или где его искать. Это как индекс в книге, только для объектов.

А что будет, если наебать систему и нарушить контракт? Всё, пиздец, Колян. Всё сломается. Смотри, какой придурок-класс можно написать:

class BrokenPerson {
    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;
        BrokenPerson that = (BrokenPerson) o;
        return age == that.age && Objects.equals(name, that.name);
    }
    // А hashCode() НЕ ПЕРЕОПРЕДЕЛИЛ! Используется дефолтный от Object!
}

И вот что получается, сука:

Set<BrokenPerson> set = new HashSet<>();
BrokenPerson p1 = new BrokenPerson("Alice", 30);
BrokenPerson p2 = new BrokenPerson("Alice", 30); // Та же самая, блядь, Алиса!

System.out.println(p1.equals(p2)); // true! Логически один человек!
set.add(p1);
System.out.println(set.contains(p2)); // А тут, ёпта, может выдать FALSE!

Почему? Да потому что p1 и p2, хоть и равные, имеют разные хэш-коды (дефолтные). HashSet посмотрел на хэш p2, пошёл не в ту корзину искать, и не нашёл нихуя. И сидит теперь, довольный, а программа не работает. Волнение ебать, терпения ноль ебать!

Как делать правильно, чтобы не быть мудаком? Очень просто, блядь. Переопределил equals — тут же, не отходя от кассы, переопределяй и hashCode. И используй для расчёта хэша те же самые поля, что и для сравнения в equals. Не выёбывайся.

Вот, смотри, пример не мудака, а адекватного человека:

class CorrectPerson {
    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;
        CorrectPerson that = (CorrectPerson) o;
        return age == that.age && Objects.equals(name, that.name); // Поля для equals
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Блядь, ТЕ ЖЕ САМЫЕ ПОЛЯ! Идеально!
    }
}

Вот так, коротко и ясно. Запомнил? equals и hashCode — как сиамские близнецы, ёпта. Где один, там и второй. Иначе получишь коллекцию, которая нихуя не работает, и будешь потом всю ночь дебажить, материться и кофе литрами хлебать. Сам от себя охуеешь.