【问题标题】:Why does changing the hashcode of an object used as a key in a HashMap make a lookup return null?为什么更改用作 HashMap 中键的对象的哈希码会使查找返回 null?
【发布时间】:2015-01-13 14:08:07
【问题描述】:

考虑以下场景:

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 呢?只有一个对象,所以key的hashcode不应该匹配吗?

【问题讨论】:

标签: java hashmap equals hashcode


【解决方案1】:

虽然hashCode() 的契约经常被描述为实现者,但有时从调用者的角度考虑它更有用:知道两个对象为@987654322 返回不同值的代码@ 有权假设它们不可能相等。虽然许多关于散列的描述都在谈论存储桶索引,但散列的问题远不止于此。

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

从上面的粗体声明及其用法和合同含义的角​​度考虑哈希码将比从桶的角度考虑要清楚得多。

【讨论】:

    【解决方案2】:

    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

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

    【讨论】:

      【解决方案3】:

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

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

      正是出于这个原因,建议从对象的不可变字段创建哈希码。

      请注意,您可以更改哈希码并可能仍然找到您的对象。您的哈希码是一个整数(一个 32 位数字),通常映射到一组更小的存储桶(例如,通过某种计算,例如hashcode % 16)。因此,您的哈希码可能会更改,但 hashcode % 16 的结果可能会给出相同的结果,因此会给出相同的存储桶。显然,这取决于实现。

      【讨论】:

        【解决方案4】:

        map.get 搜索其哈希码与正在查找的对象相同的对象。由于这 2 个对象具有不同的哈希码,它会返回 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 则返回 null

        【讨论】:

          【解决方案5】:

          这就是你必须定义的 hashCode 方法。所以假设我们有两个字段的员工类:

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

          现在,如果您刚刚设置了名称,您最终可能会得到名称的哈希码(默认 id 为 0,这将返回名称的哈希码),而如果我稍后将 id 更改为 1,那么它可能会创建另一个 hashCode xoring 名称的哈希码有 1 个。

          【讨论】:

            【解决方案6】:

            您已使用一个 hashCode 存储它,并正在使用另一个更改的 hashCode 查找它,因此您的程序的行为符合预期。这就是为什么 HashMap 的合同明确规定您不应使用其 hashCode 可以更改的键。如果我是你,我会遵循这个建议。

            【讨论】:

              猜你喜欢
              • 2013-06-20
              • 1970-01-01
              • 1970-01-01
              • 2020-12-24
              • 1970-01-01
              • 1970-01-01
              • 2015-11-20
              • 2011-03-29
              • 1970-01-01
              相关资源
              最近更新 更多