当对象 Hashcode 更改时,Hashmap 或 Hashset 中的查找会发生啥
Posted
技术标签:
【中文标题】当对象 Hashcode 更改时,Hashmap 或 Hashset 中的查找会发生啥【英文标题】:What happens to the lookup in a Hashmap or Hashset when the objects Hashcode changes当对象 Hashcode 更改时,Hashmap 或 Hashset 中的查找会发生什么 【发布时间】:2012-10-22 01:02:20 【问题描述】:在 Hashmap 中,提供的键的哈希码用于将值放置在哈希表中。在哈希集中,对象哈希码用于将值放置在底层哈希表中。也就是说,hashmap 的优点是你可以灵活地决定你想要什么作为 key,这样你就可以做这样的好事。
Map<String,Player> players = new HashMap<String,Player>();
这可以将诸如玩家姓名之类的字符串映射到玩家本身。
我的问题是,当键的 Hashcode 发生变化时,查找会发生什么变化。
我希望这对于 Hashmap 来说不是一个主要问题,因为我不希望也不希望密钥改变。在前面的例子中,如果球员的名字改变了,他就不再是那个球员了。但是,我可以使用键更改其他不是名称的字段来查找玩家,并且将来的查找将起作用。
但是在 Hashset 中,因为如果有人稍微更改对象,则使用整个对象的哈希码来放置项目,该对象的未来查找将不再解析到 Hashtable 中的相同位置,因为它依赖于整个对象的哈希码。这是否意味着一旦数据在 Hashset 中就不应更改。还是需要重新散列?还是自动完成等?怎么回事?
【问题讨论】:
我在这个问题上做了很多陈述/假设,如果有任何错误,请告诉我 【参考方案1】:在您的示例中,字符串是不可变的,因此其哈希码无法更改。但假设是,如果对象的哈希码在哈希表中作为键时确实发生了变化,那么就哈希表查找而言,它可能会消失。我在这个对相关问题的回答中更详细地介绍了:https://***.com/a/13114376/139985。 (最初的问题是关于HashSet
,但HashSet
确实是HashMap
,所以答案也涵盖了这种情况。)
可以肯定地说,如果 HashMap 或 TreeMap 的键以影响它们各自的 hashcode()
/ equals(Object)
或 compare(...)
或 compareTo(...)
合约的方式发生变化,那么数据结构将“打破”。
这是否意味着一旦数据在 Hashset 中就不应更改。
是的。
还是需要重新散列?还是自动完成等?
它不会自动重新散列。 HashMap
不会注意到密钥的哈希码已更改。事实上,当HashMap
调整大小时,您甚至不会重新计算哈希码。数据结构记住原始哈希码值,以避免在哈希表调整大小时重新计算所有哈希码。
如果您知道某个键的哈希码将要更改,则需要在更改该键之前从表中删除该条目,然后再将其添加回来。 (如果你在修改密钥后尝试remove
/ put
它,remove
可能会找不到条目。)
发生了什么事?
发生的事情是您违反了合同。不要那样做!
合同由两部分组成:
javadoc 中为 Object
指定的标准哈希码 / 等于合约。
当对象的哈希码是哈希表中的键时,它不能更改的附加约束。
HashMap
javadoc 中没有具体说明后一种约束,但Map
的javadoc 表示:
注意:如果将可变对象用作映射键,则必须非常小心。如果对象的值以影响
equals
比较的方式更改,而对象是映射中的键,则不会指定映射的行为。
影响相等性的更改(通常)也会影响哈希码。在实现级别,如果 HashMap
条目的键的哈希码发生更改,则该条目通常现在位于错误的哈希桶中,并且对于执行查找的 HashMap
方法不可见。
【讨论】:
你能解释一件事吗?如果我更改用于哈希码计算的某些字段(例如 int 10 变为 5)。然后我们将无法在映射中找到值,因为它使用的是旧版本的键。所以哈希码是不同的,对吧?但这如何违反与equals的合同?据我了解,等于仍然可以正常工作,因为它检查相同的字段。你能解释一下吗? 1) 是的。 2)它违反了合同,因为合同的一部分说你应该改变一个关键对象,当它在一个hashmap中时,它会改变它的hashcode。 equals 仍然有效的事实是无关紧要的。真正的问题是 hashmap 中的条目将位于错误的哈希链上,因此查找不会找到它。【参考方案2】:在您的示例中,键是不可变的字符串。所以键的哈希码不会改变。当键的哈希码更改未定义并导致“奇怪”行为时会发生什么。请参阅下面的示例,它打印 1、false 和 2。对象保留在集合中,但集合看起来像是损坏的(包含返回 false)。
从Set's javadoc中提取:
注意:如果将可变对象用作集合元素,则必须非常小心。如果对象的值以影响等于比较的方式更改,而对象是集合中的一个元素,则不指定集合的行为。此禁令的一个特殊情况是不允许集合包含自身作为元素。
public static void main(String args[])
Set<MyObject> set = new HashSet<>();
MyObject o1 = new MyObject(1);
set.add(o1);
o1.i = 2;
System.out.println(set.size()); //1
System.out.println(set.contains(o1)); //false
for (MyObject o : set)
System.out.println(o.i); //2
private static class MyObject
private int i;
public MyObject(int i)
this.i = i;
@Override
public int hashCode()
return i;
@Override
public boolean equals(Object obj)
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final MyObject other = (MyObject) obj;
if (this.i != other.i) return false;
return true;
【讨论】:
好吧,总结一下。 hashmap 的 Key 永远不应该改变?并且任何要放入哈希集中的对象都应该是不可变的? @Luke1111 是的,除非你想故意制造这种奇怪的行为——说实话,我想不出一个。【参考方案3】:使用 Java 的哈希,根本找不到原始引用。在当前hashcode对应的bucket中查找,没有找到。
为了事后恢复,必须迭代Hash keySet,并且必须通过迭代器删除contains
方法找不到的任何键。最好是从映射中删除键,然后用新键存储值。
【讨论】:
【参考方案4】:HashSet
由HashMap
备份。
来自 javadocs。
这个类实现了 Set 接口,由一个哈希表支持 (实际上是一个 HashMap 实例)。
因此,如果您更改哈希码,我怀疑您是否可以访问该对象。
内部实现细节
HashSet
的 add
实现是
public boolean add(E e)
return map.put(e, PRESENT)==null;
键是元素,值只是一个名为 PRESENT 的虚拟对象
contains
的实现是
public boolean contains(Object o)
return map.containsKey(o);
【讨论】:
以上是关于当对象 Hashcode 更改时,Hashmap 或 Hashset 中的查找会发生啥的主要内容,如果未能解决你的问题,请参考以下文章