Являются ли изменяемые ключи hashmap опасной практикой?

Неправильно ли использовать измененные объекты как ключи Hashmap? Что происходит, когда вы пытаетесь получить значение из Hashmap, используя ключ, который был достаточно модифицирован, чтобы изменить его хэш-код?

Например, данный

class Key
{
    int a; //mutable field
    int b; //mutable field

    public int hashcode()
        return foo(a, b);
}

с кодом

HashMap<Key, Value> map = new HashMap<Key, Value>();

Key key1 = new Key(0, 0);
map.put(key1, value1);

key1.setA(5);
key1.setB(10);

Что произойдет, если мы теперь позвоним map.get(key1)? Является ли это безопасным или целесообразным? Или поведение зависит от языка?

Ответ 1

Было отмечено многими уважаемыми разработчиками, такими как Брайан Гетц и Джош Блох, что:

Если значение hashCode() объекта может измениться в зависимости от его состояния, тогда мы должны быть осторожны при использовании таких объектов, как ключи в хэш-основе коллекций для обеспечения того, чтобы мы не разрешали изменять их состояние, когда они используются в качестве хеш-ключей. Все коллекции на основе хэшей предполагают что хэш-значение объектов не изменяется, пока оно используется как ключ в коллекции. Если хэш-код ключей должен был измениться, пока он был в коллекции, некоторые непредсказуемые и запутанные последствия может следовать. Обычно это не проблема на практике - это не обычная практика использования изменяемого объекта, такого как Список, как ключ в HashMap.

Ответ 2

Это небезопасно или желательно. Значение, отображаемое клавишей 1, никогда не может быть восстановлено. При выполнении поиска большинство хэш-карт будут делать что-то вроде

Object get(Object key) {
    int hash = key.hashCode();
    //simplified, ignores hash collisions,
    Entry entry = getEntry(hash);
    if(entry != null && entry.getKey().equals(key)) {
        return entry.getValue();
    }
    return null;
}

В этом примере key1.hashcode() теперь указывает на неправильное ведро хеш-таблицы, и вы не сможете получить значение1 с помощью ключа1.

Если вы сделали что-то вроде

Key key1 = new Key(0, 0);
map.put(key1, value1);
key1.setA(5);
Key key2 = new Key(0, 0);
map.get(key2);

Это также не будет извлекать значение1, так как key1 и key2 больше не равны, поэтому эта проверка

    if(entry != null && entry.getKey().equals(key)) 

не удастся.

Ответ 3

Это не сработает. Вы меняете значение ключа, поэтому вы в основном отбрасываете его. Это похоже на создание реального ключа жизни и блокировки, а затем изменение ключа и попытку вернуть его в замок.

Ответ 4

В хэш-картах используется хеш-код и сопоставления равенств для идентификации определенной пары ключ-значение с заданным ключом. Если карта имеет ключ в качестве ссылки на изменяемый объект, он будет работать в тех случаях, когда один и тот же экземпляр используется для извлечения значения. Однако рассмотрим следующий случай:

T keyOne = ...;
T keyTwo = ...;

// At this point keyOne and keyTwo are different instances and 
// keyOne.equals(keyTwo) is true.

HashMap myMap = new HashMap();

myMap.push(keyOne, "Hello");

String s1 = (String) myMap.get(keyOne); // s1 is "Hello"
String s2 = (String) myMap.get(keyTwo); // s2 is "Hello" 
                                        // because keyOne equals keyTwo

mutate(keyOne);

s1 = myMap.get(keyOne); // returns "Hello"
s2 = myMap.get(keyTwo); // not found

Вышеуказанное верно, если ключ хранится как ссылка. Обычно в Java это так. Например, в .NET, если ключ является типом значения (всегда передается по значению), результат будет другим:

T keyOne = ...;
T keyTwo = ...;

// At this point keyOne and keyTwo are different instances 
// and keyOne.equals(keyTwo) is true.

Dictionary myMap = new Dictionary();

myMap.Add(keyOne, "Hello");

String s1 = (String) myMap[keyOne]; // s1 is "Hello"
String s2 = (String) myMap[keyTwo]; // s2 is "Hello"
                                    // because keyOne equals keyTwo

mutate(keyOne);

s1 = myMap[keyOne]; // not found
s2 = myMap[keyTwo]; // returns "Hello"

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

Ответ 5

Если хэш-код ключей изменяется после того, как пара ключей и значений хранится в HashMap, карта не сможет получить запись.

Хэш-код ключей может измениться, если ключевой объект изменен. Mutable keys в HahsMap может привести к потере данных.

Ответ 6

Как объясняют другие, это опасно.

Способ избежать этого состоит в том, чтобы иметь константное поле, явно указывающее на хэш в ваших изменяемых объектах (так что вы бы хэш на их "идентичность", а не их "состояние" ). Вы даже можете инициализировать это поле хэша более или менее случайным образом.

Другим трюком будет использование адреса, например. (intptr_t) reinterpret_cast<void*>(this) в качестве основы для хеша.

Во всех случаях вам нужно отказаться от хэширования изменяющегося состояния объекта.

Ответ 7

Поведение карты не указывается, если значение объекта изменяется таким образом, который влияет на сравнение сравнений, а объект (Mutable) - это ключ. Даже для Set также с использованием изменяемого объекта в качестве ключа это не очень хорошая идея.

Здесь можно увидеть пример:

public class MapKeyShouldntBeMutable {

/**
 * @param args
 */
public static void main(String[] args) {
    // TODO Auto-generated method stub
    Map<Employee,Integer> map=new HashMap<Employee,Integer>();

    Employee e=new Employee();
    Employee e1=new Employee();
    Employee e2=new Employee();
    Employee e3=new Employee();
    Employee e4=new Employee();
    e.setName("one");
    e1.setName("one");
    e2.setName("three");
    e3.setName("four");
    e4.setName("five");
    map.put(e, 24);
    map.put(e1, 25);
    map.put(e2, 26);
    map.put(e3, 27);
    map.put(e4, 28);
    e2.setName("one");
    System.out.println(" is e equals e1 "+e.equals(e1));
    System.out.println(map);
    for(Employee s:map.keySet())
    {
        System.out.println("key : "+s.getName()+":value : "+map.get(s));
    }
}

  }
 class Employee{
String name;

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public boolean equals(Object o){
    Employee e=(Employee)o;
    if(this.name.equalsIgnoreCase(e.getName()))
            {
        return true;
            }
    return false;

}

public int hashCode() {
    int sum=0;
    if(this.name!=null)
    {
    for(int i=0;i<this.name.toCharArray().length;i++)
    {
        sum=sum+(int)this.name.toCharArray()[i];
    }
    /*System.out.println("name :"+this.name+" code : "+sum);*/
    }
    return sum;

}

}

Здесь мы пытаемся добавить изменяемый объект "Сотрудник" к карте. Он будет работать хорошо, если все добавленные ключи отличаются. Если у меня есть переопределенные равные и хэш-коды для класса сотрудников.

Вначале я добавил "e", а затем "e1". Для обоих из них equals() будет истинным, а hashcode будет таким же. Таким образом, карта видит, что добавляется тот же ключ, чтобы заменить старое значение значением e1. Затем мы добавили e2, e3, e4, на данный момент мы прекрасны.

Но когда мы меняем значение уже добавленного ключа i.e "e2" как единое целое, оно становится ключом, подобным ранее добавленному. Теперь карта будет вести себя проводной. В идеале e2 должен заменить существующий такой же ключ i.e e1. Но теперь карта также берет это. И вы получите это в o/p:

 is e equals e1 true
{[email protected]=28, [email protected]=27, [email protected]=25, [email protected]=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : one:value : 25

См. здесь оба ключа с одним показателем того же значения. Итак, это неожиданно. Теперь снова запустите ту же программу, изменив e2.setName("diffnt");, который здесь e2.setName("one");. Теперь o/p будет следующим:

 is e equals e1 true
{[email protected]=28, [email protected]=27, [email protected]=25, [email protected]=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : diffnt:value : null

Таким образом, добавление изменения изменяемого ключа на карте не рекомендуется.