← Назад к вопросам

Что произойдет при создании класса, используемого в качестве ключа, с неравными equals, но равными hashCode?

2.0 Middle🔥 201 комментариев
#Коллекции#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

equals и hashCode: неравные объекты с одинаковым hashCode

Это классическая ошибка в Java, которая приводит к неправильной работе HashMap, HashSet и других хеш-структур. Рассмотрим, почему это критично.

Контракт между equals и hashCode

Java предъявляет строгие требования:

// Золотое правило:
// Если a.equals(b) == true, то a.hashCode() == b.hashCode() ОБЯЗАТЕЛЬНО
// Если a.hashCode() == b.hashCode(), то a.equals(b) может быть false (коллизия)

Нарушение этого контракта приводит к непредсказуемому поведению.

Проблемный пример

public class User {
    private final int id;
    private final String name;
    
    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof User)) return false;
        User other = (User) obj;
        return this.id == other.id && this.name.equals(other.name);
        // СРАВНИВАЕМ оба поля
    }
    
    @Override
    public int hashCode() {
        return Integer.hashCode(id); // ОШИБКА! Учитываем только id
    }
}

Проблема: два объекта с одинаковым id, но разными name:

  • equals() вернёт false (так как name не совпадает)
  • hashCode() вернёт одно и то же значение (зависит только от id)

Что произойдет в HashMap

Map<User, String> map = new HashMap<>();

User user1 = new User(1, "Alice");
User user2 = new User(1, "Bob");   // Другое имя!

map.put(user1, "Data for Alice");
map.put(user2, "Data for Bob");

System.out.println(map.size()); // ОШИБКА: выведет 2, вместо 1!

Что происходит внутри HashMap:

// Шаг 1: вставляем user1
// hashCode(user1) = 1
// Добавляем в бакет[1]: user1 -> "Data for Alice"

// Шаг 2: вставляем user2
// hashCode(user2) = 1  (одинаковый!)
// Проверяем бакет[1]:
// Находим user1 в бакете
// Вызываем user1.equals(user2)?
// user1.equals(user2) = false! (разные name)
// Добавляем второй элемент в ту же бакет

// Результат: ДВА элемента в одной бакете с одинаковым hashCode!
map.size() == 2; // Вместо переписывания значения

Получение данных приведёт к ошибкам

User user1_copy = new User(1, "Alice");
String result = map.get(user1_copy);

System.out.println(result); // null или неправильные данные!

Почему?

// HashMap.get() работает так:
// 1. Вычисляет hashCode(user1_copy) = 1
// 2. Ищет в бакете[1]
// 3. Для каждого элемента в бакете проверяет equals()
// 4. Находит первый совпадающий

// Но теперь в бакете два элемента с разными name!
// Какой вернуть? Зависит от порядка итерации

Проблема в HashSet

Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);

System.out.println(set.size()); // Выведет 2, но должно быть 1
// Так как user1.equals(user2) = false, они оба добавятся

// А если ты создашь третий объект?
User user1_again = new User(1, "Alice");
System.out.println(set.contains(user1_again)); // null! Может быть false
// Даже если такой объект в set, может не найтись из-за коллизии

Визуализация проблемы

Equals = false (Alice ≠ Bob)
HashCode = одинаков (оба зависят от id=1)

 HashMap:
 ┌─────────────────────┐
 │ Бакет[1]            │
 ├─────────────────────┤
 │ User(1, Alice)      │  ← hashCode = 1
 │ User(1, Bob)        │  ← hashCode = 1 (КОЛЛИЗИЯ!)
 └─────────────────────┘
     ↓
 При поиске User(1, Alice):
 Находим бакет[1], но там 2 элемента!
 Какой вернуть?!

Правильная реализация

public class User {
    private final int id;
    private final String name;
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof User)) return false;
        User other = (User) obj;
        return this.id == other.id && this.name.equals(other.name);
    }
    
    @Override
    public int hashCode() {
        // ПРАВИЛЬНО: используем все поля из equals()
        return Objects.hash(id, name);
        // Это эквивалентно:
        // return 31 * Integer.hashCode(id) + name.hashCode();
    }
}

Теперь:

  • User(1, "Alice") и User(1, "Alice") имеют одинаковые hashCode
  • User(1, "Alice") и User(1, "Bob") имеют разные hashCode
  • HashMap работает корректно

Практический тест

// ТОД: проверяем контракт
User a = new User(1, "Alice");
User b = new User(1, "Alice");
User c = new User(1, "Bob");

// Если equals == true, hashCode должны быть одинаковыми
assert a.equals(b) == true;
assert a.hashCode() == b.hashCode(); // ОБЯЗАТЕЛЬНО true

// Если hashCode одинаковые, equals МОЖЕТ быть false
assert a.equals(c) == false;
// a.hashCode() == c.hashCode() может быть true (коллизия)

Как выбрать правильные поля для hashCode

// ✅ ПРАВИЛЬНО
// hashCode должен использовать ВСЕ поля из equals()
public class Person {
    private final String ssn;        // в equals
    private final String name;       // в equals
    private final LocalDate birth;   // в equals
    
    @Override
    public int hashCode() {
        return Objects.hash(ssn, name, birth);
    }
}

// ❌ НЕПРАВИЛЬНО
// hashCode использует МЕНЬШЕ полей, чем equals
public class Person {
    private final String ssn;        // в equals
    private final String name;       // в equals
    private final LocalDate birth;   // в equals
    
    @Override
    public int hashCode() {
        return Objects.hash(ssn); // ОШИБКА!
    }
}

Практические последствия

Map<User, String> database = new HashMap<>();

User alice1 = new User(1, "Alice");
User bob = new User(1, "Bob");     // Ошибка в коде!

database.put(alice1, "Alice's record");
database.put(bob, "Bob's record");     // Вместо обновления добавит

User alice2 = new User(1, "Alice"); // Создали новый объект
String data = database.get(alice2); // Может быть null или неправильные данные!

// Система может потерять данные или выдать неправильные результаты
// Особенно критично в:
// - Кэшах (неправильный кэш)
// - Дедупликации (дубликаты остаются)
// - Основанных на хеше алгоритмах (неправильные результаты)

Вывод

Нарушение контракта equals-hashCode приводит к:

  1. Неправильной работе HashMap — потеря данных
  2. Ошибкам в HashSet — дубликаты не удаляются
  3. Неправильным поискам — объект в коллекции не находится
  4. Трудноуловимым багам — порядок элементов случайный

Правило золотое: если класс используется как ключ в HashMap или элемент HashSet, ОБЯЗАТЕЛЬНО:

@Override
public int hashCode() {
    // Включи ВСЕ поля, которые используются в equals()
    return Objects.hash(field1, field2, field3);
}