Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Что нельзя менять в HashMap - Immutability правила
В HashMap есть строгие правила о том, что можно и нельзя изменять, особенно если это влияет на хеширование и поиск элементов.
Главное правило: ключи должны быть immutable (неизменяемые)
❌ НЕЛЬЗЯ изменять:
- Сами ключи (их состояние после добавления в HashMap)
- hashCode() ключей (вычисляется при добавлении и используется для поиска)
- equals() ключей (используется для проверки совпадения в цепочке)
Почему это важно?
public class KeyMutabilityExample {
static class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return name.hashCode(); // Хеш зависит от name
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Person)) return false;
return name.equals(((Person) obj).name);
}
}
public static void main(String[] args) {
HashMap<Person, String> map = new HashMap<>();
Person person = new Person("Alice", 25);
map.put(person, "Engineer"); // Добавляем
System.out.println(map.get(person)); // "Engineer" ✓
// ❌ ОШИБКА: изменяем ключ после добавления
person.name = "Bob"; // Меняем имя!
// Теперь hashCode вернёт другое значение
System.out.println(map.get(person)); // null ✗
// Элемент всё ещё в HashMap, но мы его не можем найти!
System.out.println(map.size()); // 1 (элемент есть!)
System.out.println(map.containsKey(person)); // false ✗
// Порча структуры HashMap
for (Person p : map.keySet()) {
System.out.println(p.name); // "Alice", но мы изменили на "Bob"!
}
}
}
Как HashMap находит элементы
Поиск в HashMap:
1. HashMap.get(key):
├─ Вычислить hash = key.hashCode()
├─ Найти бакет по индексу: index = hash & (capacity - 1)
├─ Итерировать цепочку в бакете
│ └─ Для каждого Node:
│ ├─ Если node.hash != hash → пропустить
│ └─ Если key.equals(node.key) → найдено!
└─ Если не найдено → return null
ПРОБЛЕМА: если key.hashCode() изменился, поиск пойдёт
в ДРУГОЙ бакет и элемент не будет найден!
Конкретный пример с изменением hashCode
public class HashCodeMutation {
static class MutableKey {
int value;
MutableKey(int value) {
this.value = value;
}
@Override
public int hashCode() {
return value; // Хеш зависит от value
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof MutableKey)) return false;
return value == ((MutableKey) obj).value;
}
}
public static void main(String[] args) {
HashMap<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey(10);
map.put(key, "Initial");
// Внутри HashMap:
// hash = 10
// index = 10 & (capacity - 1) = 10
// buckets[10] → Node(key, "Initial")
System.out.println(map.get(key)); // "Initial" ✓
// ❌ Изменяем ключ!
key.value = 20;
// Теперь:
// hash = 20
// index = 20 & (capacity - 1) = 4
// Ищет в buckets[4], но элемент в buckets[10]!
System.out.println(map.get(key)); // null ✗ (ОШИБКА!)
System.out.println(map.containsKey(key)); // false ✗
System.out.println(map.size()); // 1 (элемент всё ещё там!)
}
}
Что можно изменять?
✓ МОЖНО менять:
HashMap<String, Person> map = new HashMap<>();
Person person = new Person("Alice", 25);
map.put("alice-key", person);
// ✓ Можно менять сами значения
person.age = 26; // OK - это значение, а не ключ
map.put("alice-key", new Person("Bob", 30)); // OK
// ✓ Можно добавлять новые ключ-значение пары
map.put("bob-key", new Person("Bob", 30)); // OK
// ✓ Можно удалять элементы
map.remove("alice-key"); // OK
❌ НЕЛЬЗЯ менять:
HashMap<MutableKey, String> map = new HashMap<>();
MutableKey key = new MutableKey(10);
map.put(key, "Value");
// ❌ Менять состояние ключа
key.value = 20; // НЕПРАВИЛЬНО! Портит HashMap
// ❌ Менять ключ через entrySet
for (Map.Entry<MutableKey, String> entry : map.entrySet()) {
entry.getKey().value = 999; // ❌ ОПАСНО!
}
Правильная практика: immutable ключи
// ✓ Хорошо: используем immutable классы как ключи
// 1. String (immutable по умолчанию)
HashMap<String, Integer> map = new HashMap<>();
map.put("Alice", 25); // ✓ Безопасно
// 2. Integer, Long, etc (immutable примитивные обёртки)
HashMap<Integer, String> map = new HashMap<>();
map.put(1, "One"); // ✓ Безопасно
// 3. Самодельный immutable класс
public class ImmutableKey {
private final String name; // final - неизменяемо
private final int id; // final - неизменяемо
public ImmutableKey(String name, int id) {
this.name = name;
this.id = id;
}
@Override
public int hashCode() {
return Objects.hash(name, id); // На основе final полей
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ImmutableKey)) return false;
ImmutableKey other = (ImmutableKey) obj;
return name.equals(other.name) && id == other.id;
}
}
HashMap<ImmutableKey, String> map = new HashMap<>();
ImmutableKey key = new ImmutableKey("Alice", 1);
map.put(key, "Engineer"); // ✓ Безопасно
Диагностика проблемы в коде
// Если видишь такой код — это красный флаг:
HashMap<List<String>, Integer> map = new HashMap<>();
List<String> list = Arrays.asList("a", "b");
map.put(list, 100);
list.add("c"); // ❌ Список изменён! Портит HashMap
System.out.println(map.get(list)); // null или ошибка
Правило безопасности
ПРАВИЛО: Если класс используется как ключ в HashMap,
то его hashCode() и equals() должны быть основаны
ТОЛЬКО на final (неизменяемых) полях.
Проверка:
@Override
public int hashCode() {
return Objects.hash(
this.finalField1, // ✓ OK
this.finalField2 // ✓ OK
// this.mutableField // ❌ НИКОГДА!
);
}
Альтернативы
Если нужны мутируемые объекты, использовать:
// 1. TreeMap с Comparator (не зависит от hashCode)
TreeMap<MutableKey, String> map = new TreeMap<>();
// 2. Создавать новый ключ при изменении
HashMap<String, Person> map = new HashMap<>();
Person person = new Person("Alice", 25);
map.put(createKey(person), person);
// При изменении:
map.remove(createKey(person)); // Удалить старую запись
person.age = 26;
map.put(createKey(person), person); // Добавить новую
Главное правило: ключи в HashMap должны быть immutable и не менять свой hashCode() после добавления.