在 Java 中,每一个对象都有一个容易理解但是仍然有时候被遗忘或者被误用的 hashCode 方法。这里有3件事情要时刻牢记以避免常见的陷阱。
一个对象的哈希码允许算法和数据结构将对象放入隔间,就象打印机类型案件中的字母类型。打印机将所有的“A”类型放到一个房间,它寻找这个“A”的时候就只需要在这个房间进行寻找。这种简单的系统让他在未排序的抽屉中寻找类型的时候更快。这也是基于哈希的集合的想法,例如 HashMap 和 HashSet。
为了使你的类与其他基于哈希的集合或其他依赖哈希码的算法一起正常工作,所有 hashCode 的实现必须遵守一个简单的契约。
hashCode 契约
这个契约在 hashCode 方法的 JavaDoc 中进行了阐述。它可以大致的归纳为下面几点:
- 在一个运行的进程中,相等的对象必须要有相同的哈希码
- 请注意这并不意味着以下常见的误解:
- 不相等的对象一定有着不同的哈希码——错!
- 有同一个哈希值的对象一定相等——错!
这个契约允许不同的对象共享相同的哈希码,例如根据上图中的的描述,“A”和“μ”对象的哈希值就一样。在数学术语中,从对象到哈希码的映射不一定为内射或者双射。这是显而易见的,因为可能的不同对象的数量经常比可能的哈希吗的数量 (2^32)更大。
编辑:在早期的版本中,我错误的认为哈希码的映射一定属于内射,但是不一定是双射,这显然是错的。感谢 Lucian 指出这个错误。
这个约定直接导致了第一个规则:
1. 无论你何时实现 equals 方法,你必须同时实现 hashCode 方法
如果你不这样做,你将会带来损坏的对象。为什么?一个对象的 hashCode 方法需要与 equals 方法考虑同样的域。通过重写 equals 方法,你将申明一些对象与其他对象相等,但是原始的 hashCode 方法将所有的对象看做是不同的。所以你将会有不同哈希码的相同对象。例如,在 HashMap 中调用 contains 方法将会返回 false,即使这个对象已经被添加。
怎样写一个好的 hashCode 方法不在这篇文章的范围内,在 Joshua Bloch 很受欢迎的书《Effective Java》中被很好的阐释,Java 开发人员的书架上不应缺少这本书。
【你的项目需要专业意见吗?我们的 Developer Support 会为你解决问题。|在我们的 Software Craftsmanship 页面上寻找关于怎样编写简洁代码的更多提示。】
为了安全起见,让 Eclipse IDE 一次产生 equals 和 hashCode 方法: Source > Generate hashCode() and equals()….
为了保护你自己,你还可以配置 Eclipse 来检测实现了 equals 方法但是没有实现 hashCode 方法的类,并显示错误。不幸的是,此选项默认是指为“忽略”:Preferences > Java >Compiler > Errors/Warnings,然后用快速筛选器来搜索“hashcode”:
更新:正如 laurent 指出,equalsverifier 是一个强大的工具,它用来验证 hashCode 和 equals 方法的约定。您应该考虑在您的单元测试中使用它。
哈希码冲突
任何时候,两个不同对象有相同的哈希码,我们称之为冲突。冲突不要紧,它只是意味着有多个对象在同一个空间里,所以 HashMap 会再检查一遍来找正确的对象。大量的冲突将会降低系统的性能,但是它们不会导致错误的结果。
但是如果你误认为哈希码是一个对象唯一的句柄,例如使用它作为Map的key,你有时会得到错误的对象。因为虽然冲突很罕见,但他们是不可避免的。例如,字符“Aa”和“BB”产生相同的哈希码:2112。因此:
2. 永远不要把哈希码误用作一个key
你可能会反对,不像打印机的类型例子,在 Java 中,有 4,294,967,296 的空间(2^32 个可能的整型值)。40亿的插槽,发生冲突似乎是极不可能的对吗?
事实证明它不是不太可能。这是令人惊讶的冲突:请想象一下在一个房间里有 23 个随机的人。你觉得两个人是同一天生日的几率有多大 ?很低,因为一年有 365 天吗?事实上,几率是 50% 左右!50 个人是保守的估计。这个现象称为生日悖论。应用到哈希码,这意味着在 77163 个不同的对象中,有 50% 的可能性发生冲突–假设你有一个理想的哈希的函数,均匀地把对象分布在所有可用的空间里面。
例如:
安然公司的电子邮件集包含 520,924 封电子邮件。计算电子邮件内容字符串的哈希码时,我发现 50 对(甚至是 2 个三元组)不同的电子邮件有着相同的哈希码。对于五十万个字符串,这是一个很好的结果。但是这里的信息是:如果你有很多数据元素,冲突就会发生。如果你正在使用哈希码作为 key,你不会立即注意到你的错误。但是少数人会收到错误的邮件。
哈希码可变
最后,在哈希码的契约中,有一个很重要的细节是相当让人吃惊的:hashCode 并不保证在不同的应用执行中得到相同的结果。让我们看一看 Java 文档:
在一次 Java 应用的执行中,对于同一个对象,hashCode 方法必须始终返回相同的整数,但这整数不反映对象是否被修改(equals 比较)的信息。同一个应用的不同执行,该整数不必保持一致。
事实上,这是不常见的,一些类库中的类甚至指定它们用于计算哈希码的精确公式(例如字符串)。对于这些类,哈希码总是会相同。虽然大部分的哈希码的实现提供稳定的值,但你不能依赖于这一点。正如这篇文章指出的,有些类库在不同进程中会返回不同的哈希值,这有的时候会让人困惑。谷歌的 Protocol Buffers 就是一个例子。
因此,你不应该在分布式应用程序中使用哈希码。一个远程对象可能与本地对象有不同的哈希码,即使这两个对象是相等的。
3. 在分布式应用中不要使用哈希码
此外,你应该意识到从一个版本到另一个版本哈希码的功能实现可能会更改。因此您的代码不应该依赖于任何特定的哈希码值。例如,你不应该使用哈希码来持久化状态。下次你运行程序的时候,“相同”对象的哈希码可能不同。
最好的建议可能是:完全不使用哈希码,除非你自己创造了基于哈希的算法。
一种替代方法:SHA1
你可能知道加密的哈希码 SHA1 有时被用来标识对象(例如,git这样做)。这也是不安全吗?不。SHA1 使用 160 位密钥,这使得冲突几乎是不可能的。即使有很多对象,在这个空间发生冲突的几率远远低于一颗流星撞到你正在执行程序的电脑的几率。这篇文章对冲突的概率作了很好的概述