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

Что произойдет при вставке элемента c null значением, если такая пара уже была добавлена в HashMap и hashCode возвращает константу, а equals возвращает true?

2.0 Middle🔥 161 комментариев
#Коллекции

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

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

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

Вставка элемента с null в HashMap при коллизии

Вопрос касается очень важного и сложного поведения HashMap при определённых условиях. Давайте разберёмся пошагово, что произойдёт в этой ситуации.

Структура HashMap

MapMap внутренне работает с hash buckets:

// Внутренняя структура HashMap
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Node<K,V>[] table;  // Массив buckets
private int size;

private static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;           // hash(key)
    final K key;
    V value;
    Node<K,V> next;           // Для разрешения коллизий (chaining)
}

Сценарий вопроса

Дано:

  1. HashMap уже содержит пару (key1, value1)
  2. hashCode() возвращает константу (например, всегда 100)
  3. equals() возвращает true для сравнения с существующим ключом
  4. Вставляем новую пару (key1, null) — с тем же ключом, но null значением

Что произойдёт: Замена значения

В этом случае HashMap ЗАМЕНИТ значение на null!

public class ConstantHashDemo {
    static class MyKey {
        String value;
        
        public MyKey(String value) {
            this.value = value;
        }
        
        @Override
        public int hashCode() {
            return 42; // КОНСТАНТА - коллизия гарантирована
        }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof MyKey)) return false;
            MyKey other = (MyKey) obj;
            return this.value.equals(other.value); // Сравнение по значению
        }
    }
    
    public static void main(String[] args) {
        HashMap<MyKey, String> map = new HashMap<>();
        
        MyKey key1 = new MyKey("Alice");
        map.put(key1, "Alice@example.com"); // Вставляем первую пару
        System.out.println("Size: " + map.size()); // Size: 1
        System.out.println("Value: " + map.get(key1)); // Value: Alice@example.com
        
        // Вставляем с тем же ключом (по equals), но null значением
        MyKey key2 = new MyKey("Alice"); // Новый объект, но equals вернёт true
        map.put(key2, null); // Заменяем значение на null
        
        System.out.println("Size: " + map.size()); // Size: 1 (не изменился!)
        System.out.println("Value: " + map.get(key1)); // Value: null (ЗАМЕНЕНО!)
        System.out.println("Value: " + map.get(key2)); // Value: null (одна и та же пара)
    }
}

// Результат:
// Size: 1
// Value: Alice@example.com
// Size: 1
// Value: null
// Value: null

Пошаговое выполнение put()

Вот что происходит внутри HashMap.put():

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab;
    Node<K,V> p;
    int n, i;
    
    // 1. Вычисляем bucket index
    tab = table;
    n = tab.length;                        // длина массива
    i = (n - 1) & hash;                    // index в bucket
    p = tab[i];                            // существующий Node
    
    // 2. Если bucket пустой — создаём новый Node
    if (p == null) {
        tab[i] = newNode(hash, key, value, null);
    } else {
        // Bucket не пустой — проверяем коллизии
        Node<K,V> e;
        K k;
        
        // 3. Проверяем первый Node в цепи
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
            e = p; // НАЙДЕН! Это наш ключ
        } else if (p instanceof TreeNode) {
            // Red-Black дерево (если много коллизий)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        } else {
            // Проходим по цепи (linked list)
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    // Ключ не найден — создаём новый Node
                    p.next = newNode(hash, key, value, null);
                    // ... проверяем на threshold и конвертируем в дерево
                    break;
                }
                // Проверяем следующий Node
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
                    break; // НАЙДЕН ключ в цепи!
                }
                p = e;
            }
        }
        
        // 4. Если нашли существующий ключ (e != null)
        if (e != null) {
            V oldValue = e.value;          // Сохраняем старое значение
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;           // ЗАМЕНЯЕМ значение (даже если null!)
            }
            afterNodeAccess(e);
            return oldValue;               // Возвращаем старое значение
        }
    }
    
    // 5. Увеличиваем size и проверяем threshold
    ++modCount;
    if (++size > threshold) resize();
    afterNodeInsertion(evict);
    return null;                           // Новая пара
}

Ключевой момент: РАВЕНСТВО, НЕ ИДЕНТИЧНОСТЬ

Важно понять разницу:

MyKey key1 = new MyKey("Alice");
MyKey key2 = new MyKey("Alice");

// key1 == key2                          // false (разные объекты)
// key1.equals(key2)                     // true (одинаковое значение)
// map.put(key1, "value1")                // Вставляем
// map.put(key2, null)                   // ЗАМЕНЯЕМ (потому что equals!)

HashMap использует equals() и hashCode(), а НЕ ==!

Как это работает в деталях

public class DetailedExample {
    static class Key {
        int id;
        Key(int id) { this.id = id; }
        
        @Override
        public int hashCode() {
            return 42; // Коллизия ГАРАНТИРОВАНА
        }
        
        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof Key)) return false;
            return ((Key) obj).id == this.id;
        }
    }
    
    public static void main(String[] args) {
        HashMap<Key, String> map = new HashMap<>();
        
        Key k1 = new Key(1);
        Key k2 = new Key(1);
        Key k3 = new Key(2);
        
        // Вставляем три пары
        map.put(k1, "First");
        map.put(k3, "Third");
        System.out.println("After 2 puts, size: " + map.size()); // 2
        
        // k2 имеет то же значение что k1, но разный объект
        map.put(k2, null); // ЗАМЕНЯЕТ значение для k1!
        System.out.println("After put null, size: " + map.size()); // Still 2
        System.out.println("map.get(k1): " + map.get(k1));        // null
        System.out.println("map.get(k2): " + map.get(k2));        // null (одна пара!)
        System.out.println("map.get(k3): " + map.get(k3));        // "Third"
    }
}

// Результат:
// After 2 puts, size: 2
// After put null, size: 2
// map.get(k1): null
// map.get(k2): null
// map.get(k3): Third

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

В HashMap есть цепь коллизий:

Bucket 0 (индекс, вычисленный из hash):
  Node { hash=42, key=k1, value="First", next -> Node2 }
    ↓
  Node2 { hash=42, key=k3, value="Third", next=null }

Когда вставляем (k2, null):
1. Вычисляем индекс bucket (такой же, как для k1)
2. Проходим по цепи
3. Находим Node с key=k1
4. Проверяем: k2.equals(k1) → TRUE
5. ЗАМЕНЯЕМ значение: Node.value = null
6. Возвращаем старое значение "First"

Важные выводы

  1. HashMap ЗАМЕНЯЕТ значение, если ключ уже существует (по equals() и hashCode())
  2. Size НЕ изменяется — это всё равно одна пара
  3. Значение CAN быть null — HashMap поддерживает null значения
  4. hashCode() константой вызывает деградацию производительности (O(n) вместо O(1))
  5. Новый объект с equals()==true считается ТОТЖЕ КЛЮЧОМ

AntiPattern: НЕ делай так

// Плохо: hashCode() всегда возвращает одно и то же
public class BadKey {
    @Override
    public int hashCode() {
        return 0; // ОЧЕНЬ плохо - все элементы в одном bucket
    }
}

// Хорошо: используй нормальное распределение
public class GoodKey {
    private String name;
    private int age;
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age); // Хорошее распределение
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof GoodKey)) return false;
        GoodKey other = (GoodKey) obj;
        return this.name.equals(other.name) && this.age == other.age;
    }
}

Резюме

Что произойдёт:

  • HashMap ЗАМЕНИТ значение на null
  • Size остаётся 1 (не добавляется новая пара)
  • Старое значение возвращается методом put()
  • Это работает, потому что equals() вернула true, так что HashMap считает это ОДНИМ и ТЕМ ЖЕ КЛЮЧОМ

Это нормальное и ожидаемое поведение HashMap!

Что произойдет при вставке элемента c null значением, если такая пара уже была добавлена в HashMap и hashCode возвращает константу, а equals возвращает true? | PrepBro