有效和正确定义hashCode()和equals()
级别入门级
Brian Goetz
Quiotix Corp首席顾问
年 月
每个Java对象都有hashCode()和 equals()方法许多类忽略(Override)这些方法的缺省实施以在对象实例之间提供更深层次的语义可比性在Java理念和实践这一部分Java开发人员Brian Goetz向您介绍在创建Java类以有效和准确定义hashCode()和equals()时应遵循的规则和指南您可以在讨论论坛与作者和其它读者一同探讨您对本文的看法(您还可以点击本文顶部或底部的讨论进入论坛)
虽然Java语言不直接支持关联数组 可以使用任何对象作为一个索引的数组 但在根Object类中使用hashCode()方法明确表示期望广泛使用HashMap(及其前辈Hashtable)理想情况下基于散列的容器提供有效插入和有效检索直接在对象模式中支持散列可以促进基于散列的容器的开发和使用
定义对象的相等性
Object类有两种方法来推断对象的标识equals()和hashCode()一般来说如果您忽略了其中一种您必须同时忽略这两种因为两者之间有必须维持的至关重要的关系特殊情况是根据equals() 方法如果两个对象是相等的它们必须有相同的hashCode()值(尽管这通常不是真的)
特定类的equals()的语义在Implementer的左侧定义定义对特定类来说equals()意味着什么是其设计工作的一部分Object提供的缺省实施简单引用下面等式
public boolean equals(Object obj) { return (this == obj); }
在这种缺省实施情况下只有它们引用真正同一个对象时这两个引用才是相等的同样Object提供的hashCode()的缺省实施通过将对象的内存地址对映于一个整数值来生成由于在某些架构上地址空间大于int值的范围两个不同的对象有相同的hashCode()是可能的如果您忽略了hashCode()您仍旧可以使用SystemidentityHashCode()方法来接入这类缺省值
忽略 equals() 简单实例
缺省情况下equals()和hashCode()基于标识的实施是合理的但对于某些类来说它们希望放宽等式的定义例如Integer类定义equals() 与下面类似
public boolean equals(Object obj) {
return (obj instanceof Integer
&& intvalue() == ((Integer) obj)intvalue());
}
在这个定义中只有在包含相同的整数值的情况下这两个Integer对象是相等的结合将不可修改的Integer这使得使用Integer作为HashMap中的关键字是切实可行的这种基于值的Equal方法可以由Java类库中的所有原始封装类使用如IntegerFloatCharacter和Boolean以及String(如果两个String对象包含相同顺序的字符那它们是相等的)由于这些类都是不可修改的并且可以实施hashCode()和equals()它们都可以做为很好的散列关键字
为什么忽略 equals()和hashCode()?
如果Integer不忽略equals() 和 hashCode()情况又将如何?如果我们从未在HashMap或其它基于散列的集合中使用Integer作为关键字的话什么也不会发生但是如果我们在HashMap中使用这类Integer对象作为关键字我们将不能够可靠地检索相关的值除非我们在get()调用中使用与put()调用中极其类似的Integer实例这要求确保在我们的整个程序中只能使用对应于特定整数值的Integer对象的一个实例不用说这种方法极不方便而且错误频频
Object的interface contract要求如果根据 equals()两个对象是相等的那么它们必须有相同的hashCode()值当其识别能力整个包含在equals()中时为什么我们的根对象类需要hashCode()?hashCode()方法纯粹用于提高效率Java平台设计人员预计到了典型Java应用程序中基于散列的集合类(Collection Class)的重要性如HashtableHashMap和HashSet并且使用equals()与许多对象进行比较在计算方面非常昂贵使所有Java对象都能够支持 hashCode()并结合使用基于散列的集合可以实现有效的存储和检索
实施equals()和hashCode()的需求
实施equals()和 hashCode()有一些限制Object文件中列举出了这些限制特别是equals()方法必须显示以下属性
Symmetry两个引用a和 baequals(b) if and only if bequals(a)
Reflexivity所有非空引用 aequals(a)
TransitivityIf aequals(b) and bequals(c) then aequals(c)
Consistency with hashCode()两个相等的对象必须有相同的hashCode()值
Object的规范中并没有明确要求equals()和 hashCode() 必须一致 它们的结果在随后的调用中将是相同的假设不改变对象相等性比较中使用的任何信息这听起来象计算的结果将不改变除非实际情况如此这一模糊声明通常解释为相等性和散列值计算应是对象的可确定性功能而不是其它
对象相等性意味着什么?
人们很容易满足Object类规范对equals() 和 hashCode() 的要求决定是否和如何忽略equals()除了判断以外还要求其它在简单的不可修值类中如Integer(事实上是几乎所有不可修改的类)选择相当明显 相等性应基于基本对象状态的相等性在Integer情况下对象的唯一状态是基本的整数值
对于可修改对象来说答案并不总是如此清楚equals() 和hashCode() 是否应基于对象的标识(象缺省实施)或对象的状态(象Integer和String)?没有简单的答案 它取决于类的计划使用对于象List和Map这样的容器来说人们对此争论不已Java类库中的大多数类包括容器类错误出现在根据对象状态来提供equals()和hashCode()实施
如果对象的hashCode()值可以基于其状态进行更改那么当使用这类对象作为基于散列的集合中的关键字时我们必须注意确保当它们用于作为散列关键字时我们并不允许更改它们的状态所有基于散列的集合假设当对象的散列值用于作为集合中的关键字时它不会改变如果当关键字在集合中时它的散列代码被更改那么将产生一些不可预测和容易混淆的结果实践过程中这通常不是问题 我们并不经常使用象List这样的可修改对象做为HashMap中的关键字
一个简单的可修改类的例子是Point它根据状态来定义equals()和hashCode()如果两个Point 对象引用相同的(x y)座标Point的散列值来源于x和y座标值的IEEE bit表示那么它们是相等的
对于比较复杂的类来说equals()和hashCode()的行为可能甚至受到superclass或interface的影响例如List接口要求如果并且只有另一个对象是List而且它们有相同顺序的相同的Elements(由Element上的Objectequals() 定义)List对象等于另一个对象hashCode()的需求更特殊list的hashCode()值必须符合以下计算
hashCode = ;
Iterator i = erator();
while (ihasNext()) {
Object obj = inext();
hashCode = *hashCode + (obj==null ? : objhashCode());
}
不仅仅散列值取决于list的内容而且还规定了结合各个Element的散列值的特殊算法(String类规定类似的算法用于计算String的散列值)
编写自己的equals()和hashCode()方法
忽略缺省的equals()方法比较简单但如果不违反对称(Symmetry)或传递性(Transitivity)需求忽略已经忽略的equals() 方法极其棘手当忽略equals()时您应该总是在equals()中包括一些Javadoc注释以帮助那些希望能够正确扩展您的类的用户
作为一个简单的例子考虑以下类
class A {
final B someNonNullField;
C someOtherField;
int someNonStateField;
}
我们应如何编写该类的equals()的方法?这种方法适用于许多情况
public boolean equals(Object other) {
// Not strictly necessary but often a good optimization
if (this == other)
return true;
if (!(other instanceof A))
return false;
A otherA = (A) other;
return
(someNonNullFieldequals(otherAsomeNonNullField))
&& ((someOtherField == null)
? otherAsomeOtherField == null
: someOtherFieldequals(otherAsomeOtherField)));
}
现在我们定义了equals()我们必须以统一的方法来定义hashCode()一种统一但并不总是有效的定义hashCode()的方法如下
public int hashCode() { return ; }
这种方法将生成大量的条目并显着降低HashMaps的性能但它符合规范一个更合理的hashCode()实施应该是这样
public int hashCode() {
int hash = ;
hash = hash * + someNonNullFieldhashCode();
hash = hash *
+ (someOtherField == null ? : someOtherFieldhashCode());
return hash;
}
注意这两种实施都降低了类状态字段的equals()或hashCode()方法一定比例的计算能力根据您使用的类您可能希望降低superclass的equals()或ha