Czy mutable hashmap keys to niebezpieczna praktyka?

Czy używanie mutowalnych obiektów jako kluczy Hashmapowych jest złą praktyką? Co się dzieje, gdy próbujesz pobrać wartość z Hashmapy za pomocą klucza, który został zmodyfikowany na tyle, aby zmienić jej hashcode?

Na przykład, podane

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

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

Z kodem

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

Co się stanie, jeśli teraz zadzwonimy map.get(key1)? Czy to bezpieczne czy wskazane? A może zachowanie zależy od języka?

Author: Kedar Mhaswade, 2011-10-21

7 answers

Wielu szanowanych programistów, takich jak Brian Goetz i Josh Bloch, zauważyło, że:

Jeśli wartość hashCode () obiektu może się zmienić w zależności od jego stanu, to należy zachować ostrożność podczas używania takich obiektów jak klucze w oparciu o hash Kolekcje, aby upewnić się, że nie pozwolimy na zmianę ich stanu, gdy są one używane jako klucze hash. Wszystkie zbiory oparte na haszyszu zakładają że wartość hash obiektu nie zmienia się, gdy jest on używany jako klucz w kolekcji. Jeśli a kod hashowy keya miał ulec zmianie podczas był w zbiorze, niektóre nieprzewidywalne i mylące konsekwencje może za nim pójść. Zwykle nie stanowi to problemu w praktyce - nie jest powszechną praktyką jest używanie zmiennego obiektu, takiego jak lista, jako klucza w HashMap.

 60
Author: aleroot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-29 21:39:07

To nie jest bezpieczne ani wskazane. Wartość odwzorowana przez key1 nie może być nigdy odzyskana. Podczas pobierania większość map hashowych zrobi coś w stylu

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

W tym przykładzie, key1.hashcode() wskazuje teraz na błędne wiadro tabeli skrótów i nie będzie można pobrać value1 za pomocą key1.

Gdybyś zrobił coś takiego,

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

To również nie pobierze wartości1, ponieważ key1 i key2 nie są już sobie równe, więc to sprawdzenie

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

Zawiedzie.

 17
Author: sbridges,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-24 05:03:08

To nie zadziała. Zmieniasz wartość klucza, więc po prostu go wyrzucasz. To tak, jakby stworzyć prawdziwy klucz i zamek, a następnie zmienić klucz i spróbować umieścić go z powrotem w zamku.

 5
Author: onit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-20 20:57:10

Mapy skrótu używają kodu skrótu i porównań równości, aby zidentyfikować pewną parę klucz-wartość z danym kluczem. Jeśli Mapa has zachowuje klucz jako odniesienie do mutowalnego obiektu, będzie działać w przypadkach, w których ta sama instancja jest używana do pobierania wartości. Rozważmy jednak następujący przypadek:

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

Powyższe jest prawdziwe, jeśli klucz jest przechowywany jako odniesienie. W Javie zwykle tak jest. Na przykład w. NET, jeśli klucz jest typem wartości (zawsze przekazywanym przez value), wynik będzie być inny:

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"

Inne technologie mogą mieć inne zachowania. Jednak prawie wszystkie z nich doszłyby do sytuacji, w której wynik użycia mutowalnych kluczy nie jest deterministyczny, co jest bardzo złą sytuacją w aplikacji-trudną do debugowania i jeszcze trudniejszą do zrozumienia.

 5
Author: Ivaylo Slavov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-28 18:42:56

Jeśli kod skrótu klucza zmieni się po zapisaniu pary klucz-wartość w Hashmapie, mapa nie będzie w stanie pobrać tego wpisu.

Hashcode klucza może się zmienić, jeśli obiekt key jest zmienny. Zmienne klucze w HahsMap mogą spowodować utratę danych.

 4
Author: Vishal,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-04 07:52:10

Jak wyjaśniali inni, jest to niebezpieczne.

Sposobem na uniknięcie tego jest posiadanie pola const dającego wyraźnie hash w zmienialnych obiektach (więc mielibyście hash na ich "tożsamość", a nie ich "stan"). Możesz nawet zainicjować to pole hash mniej lub bardziej losowo.

Inną sztuczką byłoby użycie adresu, np. (intptr_t) reinterpret_cast<void*>(this) jako podstawy hash.

We wszystkich przypadkach musisz zrezygnować z hashowania zmieniającego się stanu obiektu.

 2
Author: Basile Starynkevitch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-28 19:27:23

Zachowanie mapy nie jest określone, jeśli wartość obiektu jest zmieniana w sposób wpływający na porównanie, podczas gdy obiekt (zmienny) jest kluczem. Nawet dla Set również używanie mutable object jako klucza nie jest dobrym pomysłem.

Zobaczmy przykład tutaj:

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;

}

}

Tutaj staramy się dodać obiekt zmienny "Pracownik" do mapy. Będzie to działać dobrze, jeśli wszystkie dodane klucze są różne.Tutaj mam nadpisane równe i hashcode dla klasy pracownika.

Najpierw dodałem "e", a potem "e1". Dla obu z nich equals () będzie true, a hashcode będzie taki sam. Map widzi tak, jakby ten sam klucz został dodany, więc powinien zastąpić starą wartość wartością e1. Następnie dodaliśmy e2, e3, e4 jesteśmy w porządku od teraz.

Ale kiedy zmieniamy wartość już dodanego klucza, czyli " e2 " jako jeden, staje się on kluczem podobnym do tego dodanego wcześniej. Teraz Mapa będzie zachowywać się przewodowo. Idealnie e2 powinien zastąpić istniejący ten sam klucz, tj. e1.Ale teraz Mapa bierze to również. I dostaniesz to w 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

Zobacz tutaj oba klucze o tej samej wartości. To nieoczekiwane.Teraz uruchom ponownie ten sam program, zmieniając e2.setName("diffnt");, czyli e2.setName("one"); tutaj ...Teraz o / p będzie takie:

 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

Dlatego dodawanie zmieniającego się klucza na mapie nie jest zalecane.

 0
Author: smruti ranjan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-11-08 16:52:17