web-dev-qa-db-fra.com

Les clés de hachage mutables sont-elles une pratique dangereuse?

Est-ce une mauvaise pratique d'utiliser des objets mutables en tant que clés Hashmap? Que se passe-t-il lorsque vous essayez d'extraire une valeur d'un tableau de hachage à l'aide d'une clé suffisamment modifiée pour changer son code de hachage?

Par exemple, étant donné

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

    public int hashcode()
        return foo(a, b);
    // setters setA and setB omitted for brevity
}

avec code

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

Key key1 = new Key(0, 0);
map.put(key1, value1); // value1 is an instance of Value

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

Que se passe-t-il si nous appelons maintenant map.get(key1)? Est-ce sûr ou conseillé? Ou le comportement dépend-il de la langue?

50
donnyton

De nombreux développeurs très respectés, tels que Brian Goetz et Josh Bloch, ont fait remarquer que:

Si la valeur hashCode () d’un objet peut changer en fonction de son état, alors doit être prudent lorsque vous utilisez des objets tels que des clés dans collections pour nous assurer que nous ne permettons pas à leur état de changer quand ils sont utilisés comme clés de hachage. Toutes les collections basées sur le hachage supposent que la valeur de hachage d’un objet ne change pas tant qu’il est utilisé en tant que clé dans la collection. Si le code de hachage d’une clé devait changer pendant que était dans une collection, des conséquences imprévisibles et déroutantes pourrait suivre. Ce n’est généralement pas un problème dans la pratique - ce n’est pas le cas pratique courante d'utiliser un objet mutable comme une liste comme clé dans un fichier HashMap.

67
aleroot

Ce n'est ni sûr ni conseillé. La valeur associée à key1 ne peut jamais être extraite. Lors de la récupération, la plupart des cartes de hachage feront quelque chose comme:

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;
}

Dans cet exemple, key1.hashcode () pointe maintenant vers le mauvais compartiment de la table de hachage et vous ne pourrez pas récupérer valeur1 avec key1.

Si vous aviez fait quelque chose comme,

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

Cela ne récupérera pas non plus value1, car key1 et key2 ne sont plus égaux, donc cette vérification

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

va échouer.

20
sbridges

Cela ne fonctionnera pas. Vous changez la valeur de la clé, vous la jetez donc essentiellement. C'est comme créer une clé et un verrou dans la vie réelle, puis changer la clé et essayer de la remettre dans la serrure. 

5
onit

Les cartes de hachage utilisent des comparaisons de code de hachage et d'égalité pour identifier une certaine paire clé-valeur avec une clé donnée. Si la carte has conserve la clé comme référence à l'objet mutable, cela fonctionnerait dans les cas où la même instance est utilisée pour extraire la valeur. Considérons cependant le cas suivant: 

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

Ce qui précède est vrai si la clé est stockée en tant que référence. C'est généralement le cas en Java. Dans .NET par exemple, si la clé est un type valeur (toujours passé par valeur), le résultat sera différent:

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"

D'autres technologies peuvent avoir d'autres comportements différents. Cependant, la quasi-totalité d'entre eux aboutiraient à une situation dans laquelle l'utilisation de clés mutables n'est pas déterministe, ce qui constitue une très mauvaise situation dans une application - difficile à déboguer et encore plus difficile à comprendre.

5
Ivaylo Slavov

Si le code de hachage de la clé change après que la paire clé-valeur (Entrée) est stockée dans HashMap, la carte ne pourra pas extraire l’entrée.

Le hashcode de la clé peut changer si l’objet clé est modifiable. Les clés mutables dans HahsMap peuvent entraîner une perte de données.

4
Vishal

Comme d'autres l'ont expliqué, c'est dangereux.

Une façon d'éviter cela est d'avoir un champ const donnant explicitement le hachage dans vos objets mutables (pour que vous puissiez hacher leur "identité", pas leur "état"). Vous pouvez même initialiser ce champ de hachage de manière plus ou moins aléatoire. 

Une autre astuce consiste à utiliser l'adresse, par exemple (intptr_t) reinterpret_cast<void*>(this) comme base pour le hachage.

Dans tous les cas, vous devez renoncer au hachage du changement d'état de l'objet.

2

Le comportement d'une mappe n'est pas spécifié si la valeur d'un objet est modifiée d'une manière qui affecte une comparaison égale alors que l'objet (modifiable) est une clé. Même pour Set, utiliser un objet mutable comme clé n’est pas une bonne idée. 

Voyons un exemple ici:

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;

}

}

Ici, nous essayons d’ajouter un objet mutable "Employé" à une carte. Cela fonctionnera bien si toutes les clés ajoutées sont distinctes. Ici, j'ai remplacé les valeurs égales et hashcode pour la classe employee.

Voir d'abord j'ai ajouté "e" puis "e1". Pour les deux, equals () sera vrai et hashcode sera le même. Donc, map voit comme si la même clé était ajoutée, elle devrait donc remplacer l'ancienne valeur par la valeur de e1. Ensuite, nous avons ajouté e2, e3, e4 et tout va bien pour le moment. 

Mais lorsque nous modifions la valeur d’une clé déjà ajoutée, c’est-à-dire "e2", elle devient une clé similaire à celle ajoutée précédemment. Maintenant, la carte se comportera de manière câblée. Idéalement, e2 devrait remplacer la même clé existante, à savoir e1.But, mais la carte prend cela aussi. Et vous obtiendrez ceci dans o/p:

 is e equals e1 true
{Employee@1aa=28, Employee@1bc=27, Employee@142=25, Employee@142=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : one:value : 25

Voir ici les deux clés ayant une montrant la même valeur également. C'est donc inattendu. Maintenant, exécutez à nouveau le même programme en changeant e2.setName("diffnt"); qui est e2.setName("one"); here ...

 is e equals e1 true
{Employee@1aa=28, Employee@1bc=27, Employee@142=25, Employee@27b=26}
key : five:value : 28
key : four:value : 27
key : one:value : 25
key : diffnt:value : null

Donc, en ajoutant le changement de la clé mutable dans une carte n'est pas encouragée. 

0
smruti ranjan