为什么更改用作HashMap中的键的对象的哈希码会使查找返回null?

时间:2014-11-16 16:56:12

标签: java hashmap equals hashcode

考虑以下情况:

Object o1 = new Object();
Object o2 = new Object();

HashMap<Object, Object> map = new HashMap<Object, Object>();
map.put(o1, o2);

boolean test1 = map.get(o1) == o2; // This evaluates to true

// Now lets say we alter the state of o1:
o1.setSomeInternalState(Object newState);

boolean test2 = map.get(o1) == o2; // This evaluates to false, because now map.get(o1) returns null

假设o1的类已覆盖equals()hashCode()

我在调试期间遇到过这个问题,因为我在一些业务逻辑中使用的一个特定对象上明确地覆盖了equalshashCode。我完全理解为什么当我改变它的状态时对象的哈希码会改变,但是为什么map.get(o1)会因为它而返回null?只有一个对象,所以密钥的哈希码不应匹配吗?

6 个答案:

答案 0 :(得分:7)

HashMap类通过哈希函数运行密钥的hashCode,将键映射到值。哈希函数用于创建桶数组的索引。例如, very 原语哈希函数将为hashCode % tableSize。更改密钥的hashCode将改变散列函数创建的索引,这意味着在该存储桶中找不到任何内容。

让我们运行一个例子,假设初始hashCode为15且表大小为4:

                         ┌----------------------┐
15 (initial hashCode) -> | hashCode % tableSize | -> index 3
                         |    (hash function)   |
                         └----------------------┘

所以让我们在索引3处插入值:

  ┌------┐
0 | null |
  |------|
1 | null |
  |------|
2 | null |
  |------|
3 | key! | <- insert
  └------┘

现在让我们修改密钥hashCode,使其现在为13:

                          ┌----------------------┐
13 (modified hashCode) -> | hashCode % tableSize | -> index 1
                          |    (hash function)   |
                          └----------------------┘

索引1是什么?没什么,null

这里简化了许多事情。在实际的哈希表实现中,哈希函数要复杂得多,以创建更均匀的分布。此外,存储桶是链接列表,因此可以处理冲突。

答案 1 :(得分:1)

您已经使用一个hashCode存储它,并使用另一个更改的hashCode查找它,因此您的程序行为符合预期。这就是为什么HashMap的合同具体说明你不应该使用hashCodes可以改变的键。如果我是你,我会遵循这个建议。

答案 2 :(得分:1)

哈希码用于存储对象,因此查找。如果您在存储对象后更改了哈希码,则查找可能会失败。

实现细节可能有所不同,但基本上基于散列的集合包含一组对象桶。哈希码指示对象存储在基于哈希的集合中的哪个存储桶(equals()方法然后标识该存储桶中的对象 - 如果您的集合被正确缩放,那么将只有一个这样的对象)。当您的哈希码更改时,您的查找很可能会在集合中找到不同的项目桶,因此您的对象似乎丢失了。

由于这个原因,建议您从对象的不可变字段创建一个哈希码。

请注意,您可以更改哈希码,但仍可能找到您的对象。您的哈希码是一个整数(一个32位数字),并且通常会映射到更小的一组桶(例如通过某种计算,例如hashcode % 16)。因此,您的哈希码可能会发生变化,但hashcode % 16的结果可能会产生相同的结果,因此也会产生相同的桶。这显然与实现有关。

答案 3 :(得分:0)

map.get搜索其哈希码与要查找的对象相同的对象。由于这两个对象具有不同的哈希码,因此它将返回null,认为此对象不在地图中

final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key);
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

如果没有对象有散列s.t. e.hash == hash then null将被返回

答案 4 :(得分:0)

你必须定义的hashCode方法。所以,让我们说我们的员工班有两个领域:

class Employee {
    int id; 
    String name;
    public int hashCode() { 
        return name.hashCode() ^ id;
    }
}

现在,如果您只是设置了名称,您最终可能会获得名称的哈希码(并且默认ID为0,这将返回名称的hashCode),而如果我稍后将ID更改为甚至为1,那么它可能会创建另一个hashCode xoring name&#39; s哈希码,带有1。

答案 5 :(得分:0)

虽然hashCode()的合同经常根据实现者的描述进行描述,但从调用者的角度考虑它有时更有用:代码知道两个对象返回了不同的值hashCode()有权假设他们不可能是平等的。虽然许多关于散列的描述都讨论了桶索引,但散列问题超出了这一点。

基本上,hashCode()的目的是为了能够快速识别出大量不可能与物品相等的东西。虽然将事物细分为哈希码符合各种标准的桶是很常见的(哈希代码符合某些标准的一堆事物不能包含哈希码不符合该标准的任何东西),但这并不是哈希码的唯一用途。如果集合类在添加项时记录项的哈希码,则可以通过首先检查它们是否包含相同的哈希值序列来检查两个实例是否包含相同的项序列。如果散列值全部匹配,则有必要单独检查项目,但是,例如,每个集合的第五十个项目的哈希值不同,没有理由详细检查前49个项目。

根据上面的粗体表述及其用法和合同含义来考虑哈希码将比在桶中考虑更清晰。