0Pricing
Java Academy · Урок

Контракт 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 — локальная установка не требуется.

Все уроки этого курса

  1. Как работает HashMap
  2. Контракт equals/hashCode
  3. Реализация hashCode
  4. Преобразование в дерево и производительность
← Назад к Java Academy