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

请考虑以下情形:

Object o1 = new Object(); Object o2 = new Object(); HashMap map = new HashMap(); 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? 只有一个对象,键的hashcode不应该匹配吗?

HashMap类通过哈希函数运行密钥的hashCode ,将键映射到值。 哈希函数用于创建存储桶数组的索引。 例如, 非常原始的哈希函数将是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是什么? 没什么,没有。

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

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

哈希码用于存储对象,因此可以查找它 。 如果在存储对象后更改哈希码,则查找失败的可能性很大。

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

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

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

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

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

现在,如果你只是设置名称,你可能最终得到名称的哈希码(并且默认id为0,这将返回名称的hashCode),而如果我以后更改id甚至为1然后它可能会创建另一个hashCode xoring名称的哈希码为1。

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

 final Entry getEntry(Object key) { int hash = (key == null) ? 0 : hash(key); for (Entry 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; } 

如果没有对象具有hash stehash == hash,则返回null

虽然hashCode()的契约经常根据实现者来描述,但从调用者的角度考虑它有时更有用: 知道两个对象返回不同值的代码, hashCode()有权假设他们不可能是平等的 。 虽然哈希的许多描述都谈论了桶指数,但哈希的问题超出了这个范围。

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

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