← Назад к вопросам
Что произойдет при вставке элемента 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)
}
Сценарий вопроса
Дано:
- HashMap уже содержит пару (key1, value1)
- hashCode() возвращает константу (например, всегда 100)
- equals() возвращает true для сравнения с существующим ключом
- Вставляем новую пару (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"
Важные выводы
- HashMap ЗАМЕНЯЕТ значение, если ключ уже существует (по equals() и hashCode())
- Size НЕ изменяется — это всё равно одна пара
- Значение CAN быть null — HashMap поддерживает null значения
- hashCode() константой вызывает деградацию производительности (O(n) вместо O(1))
- Новый объект с 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!