本文摘抄至 : Brian Goetz
摘要 : 介绍equals()和hashcode()要一起用的原理。 原文章发表于 2003年。
每个Java对象都有 hashCode() 和 equals() 方法。许多类 Override 这些方法的缺省实施,以在对象实例之间提供更深层次的语义可比性。在 Java理念和实践这一部分,Java开发人员Brian Goetz向您介绍在创建Java类以有效和准确定义 hashCode() 和 equals() 时应遵循的规则和指南。
虽然Java语言不直接支持关联数组 – 可以使用任何对象作为一个索引的数组 – 但在根 Object 类中使用 hashCode() 方法明确表示期望广泛使用 HashMap (及其前辈 Hashtable )。理想情况下基于散列的容器提供有效插入和有效检索;直接在对象模式中支持散列可以促进基于散列的容器的开发和使用。
Object 类有两种方法来推断对象的标识: equals() 和 hashCode() 。一般来说,如果您 Override 了其中一种,您必须同时 Override 这两种,因为两者之间有必须维持的至关重要的关系。特殊情况是根据 equals() 方法,如果两个对象是相等的,它们必须有相同的 hashCode() 值(尽管这通常不是真的)。
特定类的 equals() 的语义在Implementer的左侧定义;定义对特定类来说 equals() 意味着什么是其设计工作的一部分。 Object 提供的缺省实施简单引用下面等式:
public boolean equals(Object obj) { return (this == obj); }在这种缺省实施情况下,只有它们引用真正同一个对象时这两个引用才是相等的。同样, Object 提供的 hashCode() 的缺省实施通过将对象的内存地址对映于一个整数值来生成。由于在某些架构上,地址空间大于 int 值的范围,两个不同的对象有相同的 hashCode() 是可能的。如果您 Override 了 hashCode() ,您仍旧可以使用 System.identityHashCode() 方法来接入这类缺省值。
在这个定义中,只有在包含相同的整数值的情况下这两个 Integer 对象是相等的。结合将不可修改的 Integer ,这使得使用 Integer 作为 HashMap 中的关键字是切实可行的。这种基于值的Equal方法可以由Java类库中的所有原始封装类使用,如 Integer 、 Float 、 Character 和 Boolean 以及 String (如果两个 String 对象包含相同顺序的字符,那它们是相等的)。由于这些类都是不可修改的并且可以实施 hashCode() 和 equals() ,它们都可以做为很好的散列关键字。
如果 Integer 不 Override equals() 和 hashCode() 情况又将如何?如果我们从未在 HashMap 或其它基于散列的集合中使用 Integer 作为关键字的话,什么也不会发生。但是,如果我们在 HashMap中 使用这类 Integer 对象作为关键字,我们将不能够可靠地检索相关的值,除非我们在 get() 调用中使用与 put() 调用中极其类似的 Integer 实例。这要求确保在我们的整个程序中,只能使用对应于特定整数值的 Integer 对象的一个实例。不用说,这种方法极不方便而且错误频频。
Object 的interface contract要求如果根据 equals() 两个对象是相等的,那么它们必须有相同的 hashCode() 值。当其识别能力整个包含在 equals() 中时,为什么我们的根对象类需要 hashCode() ? hashCode() 方法纯粹用于提高效率。Java平台设计人员预计到了典型Java应用程序中基于散列的集合类(Collection Class)的重要性–如 Hashtable 、 HashMap 和 HashSet ,并且使用 equals() 与许多对象进行比较在计算方面非常昂贵。使所有Java对象都能够支持 hashCode() 并结合使用基于散列的集合,可以实现有效的存储和检索。
人们很容易满足Object类规范对 equals() 和 hashCode() 的要求。决定是否和如何 Override 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 754-bit表示,那么它们是相等的。
对于比较复杂的类来说, equals() 和 hashCode() 的行为可能甚至受到superclass或interface的影响。例如, List 接口要求如果并且只有另一个对象是 List, 而且它们有相同顺序的相同的Elements(由Element上的 Object.equals() 定义), List 对象等于另一个对象。 hashCode() 的需求更特殊–list的 hashCode() 值必须符合以下计算:
hashCode = 1; Iterator i = list.iterator(); while (i.hasNext()) { Object obj = i.next(); hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode()); }不仅仅散列值取决于list的内容,而且还规定了结合各个Element的散列值的特殊算法。( String 类规定类似的算法用于计算 String 的散列值。)
Override 缺省的 equals() 方法比较简单,但如果不违反对称(Symmetry)或传递性(Transitivity)需求,Override 已经 Override 的 equals() 方法极其棘手。当 Override 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 (someNonNullField.equals(otherA.someNonNullField)) && ((someOtherField == null) ? otherA.someOtherField == null : someOtherField.equals(otherA.someOtherField))); }现在我们定义了 equals() ,我们必须以统一的方法来定义 hashCode() 。一种统一但并不总是有效的定义 hashCode() 的方法如下:
public int hashCode() { return 0; }这种方法将生成大量的条目并显著降低 HashMap s的性能,但它符合规范。一个更合理的 hashCode() 实施应该是这样:
public int hashCode() { int hash = 1; hash = hash * 31 + someNonNullField.hashCode(); hash = hash * 31 + (someOtherField == null ? 0 : someOtherField.hashCode()); return hash; }注意:这两种实施都降低了类状态字段的 equals() 或 hashCode() 方法一定比例的计算能力。根据您使用的类,您可能希望降低superclass的 equals() 或 hashCode() 功能一部分计算能力。对于原始字段来说,在相关的封装类中有helper功能,可以帮助创建散列值,如 Float.floatToIntBits 。
编写一个完美的 equals() 方法是不现实的。通常,当扩展一个自身 Override 了 equals() 的instantiable类时,Override equals() 是不切实际的,而且编写将被 Override 的 equals() 方法(如在抽象类中)不同于为具体类编写 equals() 方法。