可变hashmap键是一种危险的做法吗?
使用可变对象作为Hashmap键是一种不好的做法吗?当您尝试使用修改到足以更改其哈希代码的键从哈希映射检索值时,会发生什么情况 例如,给定可变hashmap键是一种危险的做法吗?,hash,key,hashmap,hashcode,mutable,Hash,Key,Hashmap,Hashcode,Mutable,使用可变对象作为Hashmap键是一种不好的做法吗?当您尝试使用修改到足以更改其哈希代码的键从哈希映射检索值时,会发生什么情况 例如,给定 class Key { int a; //mutable field int b; //mutable field public int hashcode() return foo(a, b); // setters setA and setB omitted for brevity } 带代码 HashM
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);
HashMap map=newhashmap();
键key1=新键(0,0);
map.put(键1,值1);//value1是Value的一个实例
键1.刚毛(5);
键1.setB(10);
如果我们现在调用
map.get(key1)
,会发生什么?这是安全的还是可取的?还是行为依赖于语言?这将不起作用。您正在更改键值,因此基本上是将其丢弃。这就像创造一个现实生活中的钥匙和锁,然后改变钥匙,并试图把它放回锁 这既不安全也不可取。无法检索由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()现在指向哈希表的错误bucket,您将无法使用key1检索value1
如果你做过这样的事情
Key key1 = new Key(0, 0);
map.put(key1, value1);
key1.setA(5);
Key key2 = new Key(0, 0);
map.get(key2);
这也不会检索值1,因为键1和键2不再相等,因此此检查
if(entry != null && entry.getKey().equals(key))
将失败。哈希映射使用哈希代码和相等比较来标识具有给定密钥的某个键值对。如果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"
其他技术可能有其他不同的行为。然而,几乎所有的应用程序都会出现使用可变键的结果不确定的情况,这在应用程序中是非常糟糕的情况——难以调试,甚至更难理解。正如其他人所解释的,这是危险的 避免这种情况的一种方法是使用const字段显式地给出可变对象中的散列(因此您将根据它们的“标识”而不是“状态”进行散列)。您甚至可以或多或少随机初始化该散列字段 另一个技巧是使用地址,例如,
(intptr\u t)重新解释(this)
作为散列的基础
在所有情况下,您都必须放弃对对象不断变化的状态进行哈希运算。许多备受尊敬的开发人员,如Brian Goetz和Josh Bloch,都注意到: 如果对象的hashCode()值可以根据其状态进行更改,那么 在基于哈希的应用程序中使用此类对象作为键时必须小心 集合,以确保在 它们被用作散列键。所有基于哈希的集合都假定 对象的哈希值在用作 输入集合中的键。如果密钥的散列码在 在一个集合中,一些不可预测和混乱的结果 我可以跟着。在实践中,这通常不是问题——事实并非如此 使用列表之类的可变对象作为 哈希映射
如果在将键值对(条目)存储在HashMap中后,键的哈希代码发生更改,则该映射将无法检索该条目
如果Key对象是可变的,则Key的hashcode可以更改。HahsMap中的可变键可能会导致数据丢失。如果对象的值在对象(可变)为键时以影响相等比较的方式更改,则不会指定映射的行为。即使对于集合,也使用可变对象作为键不是一个好主意 让我们看一个例子:
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;
}
}
请参见此处的两个键,其中一个键也显示相同的值。所以这是出乎意料的。现在通过更改e2.setName(“diffnt”)再次运行相同的程序代码>即e2.setName(“一”)代码>此处…现在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
因此,不鼓励在映射中添加或更改可变键 我不会重复别人说过的话。是的,这是不可取的。但在我看来,文档中的说明并不明显
您可以在以下网站上找到它:
注意:如果使用可变对象作为映射,则必须非常小心
钥匙。如果对象的值
以影响相等比较的方式更改,而
对象是地图中的一个关键点
根据您对行为的期望,可变键可能会出现两个截然不同的问题
第一个问题:(可能是最微不足道的——但它给了我一些我没有想到的问题!)
您正试图通过更新和修改相同的键对象,将键值对放置到映射中。你可以做一些类似于Map
的事情,然后简单地说:
int key = 0;
loop {
map.put(key++, newString);
}
我正在重用“object”键
来创建地图。这在Java中工作得很好,因为在自动装箱中,key
的每个新值都自动装箱到一个新的整数对象。如果我创建了自己的(可变)整数对象,则不起作用:
MyInteger {
int value;
plusOne(){
value++;
}
}
然后尝试了同样的方法:
MyInteger key = new MyInteger(0);
loop{
map.put(key.plusOne(), newString)
}
例如,我期望映射0->“a”
和1->“b”
。在第一个示例中,如果我更改int key=0
,映射将(正确地)给我“a”
。为简单起见,我们假设MyInteger
总是返回相同的hashCode()
(如果您能够设法为对象的所有可能状态创建唯一的hashCode值,这将不是问题,您应该获得奖励)。在本例中,我调用0->“a”
,因此现在映射持有我的键
,并将其映射到“a”
,然后修改key=1
,并尝试放置1->“b”
。我们有麻烦了!hashCode()
是相同的,HashMap中唯一的键是我的MyInteger键
对象,它刚刚被修改为
MyInteger key = new MyInteger(0);
loop{
map.put(key.plusOne(), newString)
}
map.get(0) map1: 0 -> null, map2: 0 -> a, map3: 0 -> a
map.get(1) map1: 1 -> null, map2: 1 -> b, map3: 1 -> b
map.get(2) map1: 2 -> c, map2: 2 -> a, map3: 2 -> c
map.get(3) map1: 3 -> null, map2: 3 -> null, map3: 3 -> null
public class HashMapProblems {
private int value = 0;
public HashMapProblems() {
this(0);
}
public HashMapProblems(final int value) {
super();
this.value = value;
}
public void setValue(final int i) {
this.value = i;
}
@Override
public int hashCode() {
return value % 2;
}
@Override
public boolean equals(final Object o) {
return o instanceof HashMapProblems
&& value == ((HashMapProblems) o).value;
}
@Override
public Object clone() {
return new HashMapProblems(value);
}
public void reset() {
this.value = 0;
}
public static void main(String[] args) {
final HashMapProblems k0 = new HashMapProblems(0);
final HashMapProblems k1 = new HashMapProblems(1);
final HashMapProblems k2 = new HashMapProblems(2);
final HashMapProblems k = new HashMapProblems();
final HashMap<HashMapProblems, String> map1 = firstProblem(k);
final HashMap<HashMapProblems, String> map2 = secondProblem(k0, k1, k2);
final HashMap<HashMapProblems, String> map3 = secondProblemFixed(k0, k1, k2);
for (int i = 0; i < 4; ++i) {
k0.setValue(i);
System.out.printf(
"map.get(%d) map1: %d -> %s, map2: %d -> %s, map3: %d -> %s",
i, i, map1.get(k0), i, map2.get(k0), i, map3.get(k0));
System.out.println();
}
}
private static HashMap<HashMapProblems, String> firstProblem(
final HashMapProblems start) {
start.reset();
final HashMap<HashMapProblems, String> map = new HashMap<>();
map.put(start, "a");
start.setValue(1);
map.put(start, "b");
start.setValue(2);
map.put(start, "c");
return map;
}
private static HashMap<HashMapProblems, String> secondProblem(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length).forEach(
index -> map.put(keys[index], "" + (char) ('a' + index)));
return map;
}
private static HashMap<HashMapProblems, String> secondProblemFixed(
final HashMapProblems... keys) {
final HashMap<HashMapProblems, String> map = new HashMap<>();
IntStream.range(0, keys.length)
.forEach(index -> map.put((HashMapProblems) keys[index].clone(),
"" + (char) ('a' + index)));
return map;
}
}