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

Что произойдет при использовании mutable объекта в качестве ключа и изменении его состояния?

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

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

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

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

Mutable объекты как ключи в HashMap/HashSet

Критическая проблема: если использовать изменяемый (mutable) объект как ключ в HashMap или HashSet и потом изменить его состояние, ты получишь потерю данных и неопределённое поведение.

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

HashMap работает на основе хеш-кодов (hash codes):

  1. При добавлении объекта вычисляется его hashCode()
  2. Объект помещается в "корзину" по этому хешу
  3. Если потом изменить объект, его hashCode() может измениться
  4. HashMap больше не найдёт объект в нужной корзине

Демонстрация проблемы

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public void setName(String name) {
        this.name = name;  // изменяем состояние!
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);  // зависит от name и age
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
    
    @Override
    public String toString() {
        return "Person(" + name + ", " + age + ")";
    }
}

public class MutableKeyProblem {
    public static void main(String[] args) {
        // ПРОБЛЕМНЫЙ КОД
        Map<Person, String> map = new HashMap<>();
        
        Person john = new Person("John", 30);
        map.put(john, "Engineer");
        
        System.out.println("После добавления:");
        System.out.println("map.size(): " + map.size());  // 1
        System.out.println("map.get(john): " + map.get(john));  // Engineer
        
        // Изменяем объект!
        john.setName("James");
        
        System.out.println("\nПосле изменения имени:");
        System.out.println("john.hashCode(): " + john.hashCode());  // ИЗМЕНИЛСЯ!
        System.out.println("map.size(): " + map.size());  // Ещё 1
        System.out.println("map.get(john): " + map.get(john));  // null (!)
        
        System.out.println("\nПроблема: данные потеряны!");
        System.out.println("Можем ли мы найти значение? " + 
            map.containsValue("Engineer"));  // true, но ключ потерян
    }
}

Вывод программы:

После добавления:
map.size(): 1
map.get(john): Engineer

После изменения имени:
john.hashCode(): новое значение
map.size(): 1
map.get(john): null

Проблема: данные потеряны!
Можем ли мы найти значение? true

Почему это случилось?

// При добавлении:
Person john = new Person("John", 30);
int hashCode1 = john.hashCode();  // Хеш на основе "John"
map.put(john, "Engineer");  // Объект в корзине hashCode1

// После изменения:
john.setName("James");
int hashCode2 = john.hashCode();  // Хеш на основе "James" - ДРУГОЙ!

// Попытка получить значение:
map.get(john);  // Ищет в корзине hashCode2, но объект в hashCode1!
               // Результат: null

Ещё один пример: HashSet

public class HashSetProblem {
    public static void main(String[] args) {
        Set<Person> set = new HashSet<>();
        
        Person person = new Person("Alice", 25);
        set.add(person);
        
        System.out.println("set.contains(person): " + set.contains(person));  // true
        System.out.println("set.size(): " + set.size());  // 1
        
        // Изменяем объект
        person.setName("Bob");
        
        System.out.println("\nПосле изменения:");
        System.out.println("set.contains(person): " + set.contains(person));  // false (!)
        System.out.println("set.size(): " + set.size());  // 1
        
        // Объект всё ещё в set, но мы не можем его найти!
        set.forEach(System.out::println);  // Bob, 25
        // Но contains() вернёт false!
    }
}

РЕШЕНИЕ 1: Используй immutable объекты

// Правильно: неизменяемый класс
public final class ImmutablePerson {
    private final String name;
    private final int age;
    
    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Нет setters! Только getters
    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
    
    // hashCode и equals не изменяются
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        ImmutablePerson other = (ImmutablePerson) obj;
        return age == other.age && Objects.equals(name, other.name);
    }
}

public class Solution1 {
    public static void main(String[] args) {
        Map<ImmutablePerson, String> map = new HashMap<>();
        
        ImmutablePerson john = new ImmutablePerson("John", 30);
        map.put(john, "Engineer");
        
        System.out.println("map.get(john): " + map.get(john));  // Engineer
        
        // Если нужно изменить, создаём новый объект
        ImmutablePerson james = new ImmutablePerson("James", 30);
        map.put(james, map.get(john));
        
        System.out.println("map.get(james): " + map.get(james));  // Engineer
    }
}

РЕШЕНИЕ 2: Используй String или Integer как ключи

// String и Integer - immutable по своей природе
public class SafeKeyExample {
    public static void main(String[] args) {
        Map<String, Person> map = new HashMap<>();
        
        Person john = new Person("John", 30);
        map.put("john", john);  // String как ключ
        
        System.out.println("Before: " + map.get("john"));  // Person(John, 30)
        
        john.setName("James");  // Можем менять объект-значение
        
        System.out.println("After: " + map.get("john"));  // Person(James, 30)
        // Всё работает, потому что ключ (String) не изменился!
    }
}

РЕШЕНИЕ 3: Используй копию как ключ

public class CopyKeyExample {
    public static void main(String[] args) {
        Map<Person, String> map = new HashMap<>();
        
        Person original = new Person("John", 30);
        // Создаём копию для использования как ключ
        Person keyVersion = new Person(original.getName(), original.getAge());
        
        map.put(keyVersion, "Engineer");
        
        // Изменяем оригинал
        original.setName("James");
        
        // Ключ не изменился
        System.out.println(map.get(keyVersion));  // Engineer
    }
}

Best Practice: Immutable класс для ключей

// Используй Record в Java 16+
public record PersonKey(String name, int age) { }

public class RecordExample {
    public static void main(String[] args) {
        Map<PersonKey, String> map = new HashMap<>();
        
        PersonKey key = new PersonKey("John", 30);
        map.put(key, "Engineer");
        
        System.out.println(map.get(key));  // Engineer
        // Records автоматически immutable и имеют правильный hashCode/equals
    }
}

Или используй Lombok

import lombok.Value;

@Value  // Это делает класс immutable
public class PersonKey {
    String name;
    int age;
}

public class LombokExample {
    public static void main(String[] args) {
        Map<PersonKey, String> map = new HashMap<>();
        
        PersonKey key = new PersonKey("John", 30);
        map.put(key, "Engineer");
        
        System.out.println(map.get(key));  // Engineer
        // @Value автоматически генерирует hashCode, equals, toString
    }
}

Правило для equals() и hashCode()

// Контракт: если два объекта equals(), их hashCode() должны быть одинаковыми
if (obj1.equals(obj2)) {
    // гарантировано: obj1.hashCode() == obj2.hashCode()
}

// Для mutable объектов это невозможно соблюсти,
// потому что equals может измениться, но hashCode мог бы остаться тем же
public class ContractExample implements Comparable<ContractExample> {
    private int value;
    
    public void setValue(int value) {
        this.value = value;  // НИКОГДА не делай это для ключей!
    }
    
    @Override
    public int hashCode() {
        return value;  // зависит от состояния
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        ContractExample other = (ContractExample) obj;
        return value == other.value;  // зависит от состояния
    }
    
    @Override
    public int compareTo(ContractExample o) {
        return Integer.compare(this.value, o.value);
    }
}

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

  • НИКОГДА не используй mutable объекты как ключи в HashMap, HashSet, ConcurrentHashMap и др.
  • Если изменить mutable ключ, его hashCode() может измениться
  • Результат: объект потеряется в структуре данных
  • Используй immutable объекты (String, Integer, Record, @Value)
  • Если нужен mutable ключ, используй копию как ключ
  • Следуй контракту: если equals() зависит от поля, то и hashCode() должен зависеть

Это один из самых тонких и опасных багов в Java!

Что произойдет при использовании mutable объекта в качестве ключа и изменении его состояния? | PrepBro