web-dev-qa-db-ja.com

可変ハッシュマップキーは危険なプラクティスですか?

可変オブジェクトをハッシュマップキーとして使用するのは悪い習慣ですか?ハッシュコードを変更するのに十分に変更されたキーを使用してハッシュマップから値を取得しようとするとどうなりますか?

たとえば、与えられた

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

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

コード付き

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

map.get(key1)を呼び出すとどうなりますか?これは安全ですか?または、動作は言語に依存していますか?

57
donnyton

Brian GoetzやJosh Blochなど、多くの尊敬されている開発者が次のように指摘しています。

オブジェクトのhashCode()値がその状態に基づいて変化する可能性がある場合、そのようなオブジェクトをハッシュベースのコレクションのキーとして使用するときは、ハッシュキーとして使用されているときに状態が変化しないように注意する必要があります。すべてのハッシュベースのコレクションは、オブジェクトのハッシュ値がコレクション内のキーとして使用されている間は変更されないと想定しています。キーがコレクション内にあるときにキーのハッシュコードが変更されると、予測不能で混乱を招く結果が生じる可能性があります。通常、これは実際には問題ではありません。リストのような可変オブジェクトをHashMapのキーとして使用することは一般的ではありません。

76
aleroot

これは安全でもお勧めでもありません。 key1によってマップされた値は取得できません。取得を行うとき、ほとんどのハッシュマップは次のようなことをします

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()がハッシュテーブルの間違ったバケットを指すようになり、key1でvalue1を取得できなくなります。

次のようなことをした場合、

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

Key1とkey2は等しくないため、これはvalue1も取得しません。

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

失敗します。

23
sbridges

ハッシュマップは、ハッシュコードと等値比較を使用して、特定のキーを持つ特定のキーと値のペアを識別します。 hasマップがキーを可変オブジェクトへの参照として保持している場合、同じインスタンスを使用して値を取得する場合に機能します。ただし、次の場合を考慮してください。

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"

他のテクノロジーには、他の異なる動作がある場合があります。ただし、それらのほとんどすべてが、可変キーを使用した結果が決定論的ではない状況になります。これは、アプリケーションでは非常に悪い状況です。デバッグが難しく、さらに理解しにくいです。

6
Ivaylo Slavov

これは機能しません。キー値を変更しているため、基本的には破棄しています。実際のキーとロックを作成してからキーを変更し、ロックに戻そうとするようなものです。

5
onit

キーと値のペア(エントリ)がHashMapに保存された後にキーのハッシュコードが変更されると、マップはエントリを取得できなくなります。

キーオブジェクトが変更可能な場合、キーのハッシュコードは変更できます。 HahsMapの可変キーは、データの損失につながる可能性があります。

4
Vishal

他の人が説明したように、それは危険です。

これを回避する方法は、可変オブジェクトのハッシュを明示的に指定するconstフィールドを持つことです(したがって、「状態」ではなく「アイデンティティ」でハッシュします)。ハッシュフィールドを多少ランダムに初期化することもできます。

別のトリックは、アドレスを使用することです。 (intptr_t) reinterpret_cast<void*>(this)ハッシュの基礎として。

すべての場合において、オブジェクトの状態の変化をハッシュ化することをあきらめなければなりません。

Object(Mutable)がキーであるときに、等しい比較に影響する方法でオブジェクトの値が変更された場合、Mapの動作は指定されません。 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;

}

}
_

ここでは、可変オブジェクト「Employee」をマップに追加しようとしています。追加されたすべてのキーが明確であれば、うまく機能します。ここで、従業員クラスの等号とハッシュコードをオーバーライドしました。

最初に「e」を追加し、次に「e1」を追加しました。両方ともequals()がtrueになり、ハッシュコードが同じになります。したがって、マップは同じキーが追加されているように見えるため、古い値をe1の値に置き換える必要があります。次に、e2、e3、e4を追加しました。現在は問題ありません。

しかし、すでに追加されているキーの値、つまり「e2」を1つとして変更する場合、それは以前に追加されたキーと同様のキーになります。これで、マップは有線で動作します。理想的には、e2は既存の同じキーを置き換える必要があります(e1)。そして、これは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
_

同じ値を示すキーを持つ両方のキーを参照してください。したがって、その予期しない。今ここでe2.setName("diffnt");であるe2.setName("one");を変更して同じプログラムを再度実行します。

_ 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
_

そのため、マップ内の変更可能なキーの変更を追加することは推奨されません。

0
smruti ranjan