可变的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)
会怎么样? 这是安全还是可取的? 或者是依赖于语言的行为?
许多备受尊敬的开发人员,如Brian Goetz和Josh Bloch指出:
如果一个对象的hashCode()的值可以根据它的状态而改变,那么当我们在基于散列的集合中使用这些对象作为键时,我们必须小心,以确保当它们被用作散列键时不会改变它们的状态。 所有基于散列的集合都假定对象的散列值在集合中用作键时不会更改。 如果一个密钥的哈希码在集合中发生变化,可能会出现一些不可预知的混乱后果。 这在实践中通常不是问题 – 在HashMap中使用像List这样的可变对象作为键是不常见的做法。
这是不安全或不可取的。 无法检索由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);
这也不会检索value1,因为key1和key2不再相等,所以这个检查
if(entry != null && entry.getKey().equals(key))
会失败。
这是行不通的。 你正在改变关键的价值,所以你基本上把它扔掉了。 它就像创造一个真实的钥匙和锁,然后改变钥匙,并试图把它放回锁。
哈希映射使用哈希码和相等比较来识别给定键的某个键值对。 如果has map将key保存为可变对象的引用,那么在使用同一个实例来检索该值的情况下,它将起作用。 但是,请考虑以下情况:
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中,例如,如果键是一个值types(总是按值传递),结果将会不同:
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"
其他技术可能有其他不同的行为。 然而,几乎所有的人都会遇到这样的情况:使用可变密钥的结果是不确定的,这在应用中是非常糟糕的情况 – 难以debugging,甚至更难理解。
如果在键值对(Entry)存储在HashMap之后密钥的哈希码发生了变化,则映射将无法检索该条目。
密钥的哈希码可以改变,如果密钥对象是可变的。 HahsMap中的可变键可能导致数据丢失。
正如别人解释,这是危险的。
避免这种情况的一种方法是在你的可变对象中有一个const字段明确地给出散列值(这样你就可以散列他们的“身份”,而不是他们的“状态”)。 你甚至可以随机地初始化这个散列字段。
另一个技巧是使用地址,例如(intptr_t) reinterpret_cast<void*>(this)
作为散列的基础。
在任何情况下,你都不得不放弃对象的变化状态。
如果对象的值以影响等于比较的方式更改,而对象(可变)是关键字,则不会指定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”添加到地图中。 如果所添加的所有密钥都不相同,那么它将工作得很好。在这里,我重写了员工类的equals和hashcode。
先看看我已经加了“e”,然后加了“e1”。 对于它们两个,equals()将是true,并且hashcode将是相同的。 所以map看起来好像是相同的键被添加了,所以它应该用e1的值来replace旧的值。 然后我们加了e2,e3,e4我们现在好了。
但是,当我们将一个已经添加的密钥(即“e2”)的价值改变为一个密钥时,它就变成了与之前添加的密钥类似的密钥。 现在地图将会有线。 理想情况下,e2应该取代现有的相同的键,即e1.But现在地图也采取了这一点。 你会得到这个在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");
这里…现在o / p将是这样的:
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
所以通过添加更改地图中的可变键是不鼓励的。