当对象 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 指定的标准哈希码 / 等于合约。

    当对象的哈希码是哈希表中的键时,它不能更改的附加约束。

HashMapjavadoc 中没有具体说明后一种约束,但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】:

HashSetHashMap 备份。

来自 javadocs

这个类实现了 Set 接口,由一个哈希表支持 (实际上是一个 HashMap 实例)。

因此,如果您更改哈希码,我怀疑您是否可以访问该对象。

内部实现细节

HashSetadd 实现是

 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 中的查找会发生啥的主要内容,如果未能解决你的问题,请参考以下文章

hashmap实现原理2

hashMap

关于HashMap

HashMap面试题

HashMap知识梳理

(转)HashMap底层实现原理