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

Что будет происходить при вставке в HashMap двух одинаковых элементов по equals с разным значением hashCode?

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

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

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

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

HashMap: equals и hashCode контракт

Вопрос: Что будет если вставить два элемента где equals=true, но hashCode разный?

Ответ: Это нарушение контракта и приведет к неправильной работе HashMap. Оба элемента будут в разных buckets.

HashMap внутреннее устройство

HashMap использует двухуровневую систему:

1. hashCode() → индекс bucket-a (корзины)
2. equals() → поиск внутри bucket-a

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

public class BadKey {
    private String value;
    
    public BadKey(String value) {
        this.value = value;
    }
    
    // ❌ ПЛОХО: equals говорит они равны
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof BadKey)) return false;
        return value.equals(((BadKey) o).value);
    }
    
    // ❌ ПЛОХО: hashCode нарушает контракт
    @Override
    public int hashCode() {
        return 42; // Всегда одно и то же (очень плохо!)
        // Но в нашем примере у разных объектов могут быть разные коды
    }
}

public class Problem {
    public static void main(String[] args) {
        HashMap<BadKey, String> map = new HashMap<>();
        
        BadKey key1 = new BadKey("John") {
            @Override
            public int hashCode() {
                return 1; // hashCode = 1
            }
        };
        
        BadKey key2 = new BadKey("John") {
            @Override
            public int hashCode() {
                return 2; // Такой же по equals, но hashCode = 2 !!!
            }
        };
        
        System.out.println("key1.equals(key2): " + key1.equals(key2)); // true
        System.out.println("key1.hashCode(): " + key1.hashCode());     // 1
        System.out.println("key2.hashCode(): " + key2.hashCode());     // 2
        
        // Вставляем первый ключ
        map.put(key1, "Value 1");
        
        // Вставляем второй ключ
        map.put(key2, "Value 2");
        
        // Проблема: оба ключа в HashMap!
        System.out.println("Map size: " + map.size()); // 2 ❌ Должно быть 1
        
        // Получение значения
        System.out.println("Get by key2: " + map.get(key2));       // "Value 2"
        System.out.println("Get by key1: " + map.get(key1));       // "Value 1" ❌ Неправильно!
    }
}

Почему это происходит

HashMap алгоритм поиска:

1. Вычисляем hash() от ключа
2. Вычисляем индекс bucket-a = hash & (capacity - 1)
3. Идем в эту корзину
4. Ищем элемент с equals() == true

Если hashCode разный:
→ Идем в разные bucket-ы
→ equals никогда не вызывается
→ Оба элемента добавляются в HashMap

Диаграмма

HashMap с capacity=16:

Букет 0: []
Букет 1: [key1="John", "Value 1"] ← hashCode()=1
Букет 2: [key2="John", "Value 2"] ← hashCode()=2 ❌ РАЗНЫЕ БУКЕТЫ!
Букет 3: []
Букет 4: []
...
Букет 15: []

Map.size() = 2 вместо 1

Контракт equals/hashCode

Правило: Если a.equals(b) == true, то a.hashCode() ДОЛЖЕН равняться b.hashCode()

// ✓ ПРАВИЛЬНО
public class GoodKey {
    private String value;
    
    public GoodKey(String value) {
        this.value = value;
    }
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof GoodKey)) return false;
        return value.equals(((GoodKey) o).value);
    }
    
    @Override
    public int hashCode() {
        return value.hashCode(); // Зависит от value, как и equals
    }
}

public class CorrectBehavior {
    public static void main(String[] args) {
        HashMap<GoodKey, String> map = new HashMap<>();
        
        GoodKey key1 = new GoodKey("John");
        GoodKey key2 = new GoodKey("John");
        
        System.out.println("key1.equals(key2): " + key1.equals(key2)); // true
        System.out.println("key1.hashCode() == key2.hashCode(): " +
            (key1.hashCode() == key2.hashCode())); // true ✓
        
        map.put(key1, "Value 1");
        map.put(key2, "Value 2");
        
        System.out.println("Map size: " + map.size()); // 1 ✓ Правильно
        System.out.println("Map.get(key1): " + map.get(key1)); // "Value 2" ✓
    }
}

Хеш-коллизии (нормальные)

Если hashCode у двух разных объектов одинаков (но equals=false):

public class HashCollision {
    @Override
    public int hashCode() {
        return 42; // Очень плохая hash функция
    }
    
    @Override
    public boolean equals(Object o) {
        return this == o; // Разные объекты = не равны
    }
}

// HashMap поместит оба в одну корзину (bucket)
// Но они различны по equals(), поэтому оба остаются
// (хеш-коллизия обработана через linked list или red-black tree)

HashMap.put(collision1, "A");
HashMap.put(collision2, "B");

// Оба в корзине 42, но разные узлы

Разные хеши, но equals=true (ОШИБКА)

Это нарушение контракта — приводит к нарушению инвариантов HashMap:

HashMap<BadKey, String> map = new HashMap<>();

BadKey a = new BadKey("x") { hashCode() { return 1; } };
BadKey b = new BadKey("x") { hashCode() { return 2; } }; // Разные коды!

map.put(a, "Value A");
map.put(b, "Value B");

// ❌ Инвариант HashMap нарушен:
map.size(); // 2 (должно быть 1)
map.containsKey(a); // true
map.containsKey(b); // true (они по equals равны, но в разных bucket-ах)
map.values(); // Содержит оба значения

Как это найти (IDE помощь)

// IntelliJ подчеркнет:
@Override
public boolean equals(Object o) { ... } // ⚠️ hashCode не согласован

// Правильное решение:
@Override
public int hashCode() {
    return Objects.hash(field1, field2);
}

@Override
public boolean equals(Object o) {
    if (!(o instanceof MyClass)) return false;
    MyClass that = (MyClass) o;
    return Objects.equals(field1, that.field1) &&
           Objects.equals(field2, that.field2);
}

Best Practices

// ✓ Всегда use IDE generation
@Data // Lombok генерирует оба
public class User {
    private String id;
    private String name;
}

// ✓ Используй Objects.hash() и Objects.equals()
public class Product {
    @Override
    public int hashCode() {
        return Objects.hash(id, sku); // Правильно
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Product)) return false;
        Product product = (Product) o;
        return Objects.equals(id, product.id) &&
               Objects.equals(sku, product.sku);
    }
}

// ✗ Не используй случайные хеши
public class Bad {
    @Override
    public int hashCode() {
        return new Random().nextInt(); // ❌ НИКОГДА!
    }
}

На собеседовании

Правильный ответ:

"Это нарушение контракта equals/hashCode. HashMap использует hashCode() для выбора bucket-а, и если hashCode разный, элементы пойдут в разные bucket-ы, даже если equals говорит что они равны.

Результат: HashMap будет содержать оба элемента (вместо одного), что нарушает инвариант.

Контракт: если a.equals(b) == true, то a.hashCode() == b.hashCode().

Это правило обязательно, иначе HashMap (и HashSet, HashMap.keySet()) дают неправильные результаты."

Ключевые выводы

  • Контракт: equals=true → hashCode ДОЛЖЕН быть одинаков
  • Нарушение: приводит к дублям в HashMap
  • hashCode определяет bucket (быстро)
  • equals сравнивает внутри bucket-а (медленно)
  • Всегда генерируй оба через IDE или Lombok
  • Используй Objects.hash() и Objects.equals()
  • Это частая ошибка и один из любимых вопросов на собеседованиях