原文地址:http://www.geocities.com/technofundo/tech/java/equalhash.html
http://blog.163.com/cosion@126/blog/static/3415796420087711534555/
前言
在Java语言中,所有类的父类 java.lang.Object 中有两个非常重要的方法。
·
·
在用户类和其他类进行比较的时候或被添加到集合中的时候,这两个方法的重要性就被体现出来了。这两个方法已经成为了 SCJP 1.4认证的题目。本文将向有志于SCJP 1.4认证的人提供一些关于这两个方法的必要信息,即使你对此认证并无兴趣,也可以帮助你理解这两个方法的机制,并在你自己的类中实现这些方法。
public boolean equals(Object obj)
这个方法是用来判断调用equals方法的对象与被当作参数传入的其他对象是否相等。equals方法在Object类中的默认实现是仅仅是判断两个对象的引用x和y是否指向同一个对象即判断x == y是否成立。这个特殊的比较即“浅比较”。然而,自己实现了equals方法的类应该实现“深比较”,即比较相应的数据内容。因为Object类中没有属性,所以使用“浅比较”做简单实现。
以下是JDK 1.4 API文档中关于Object类equals方法的协定:
指出某个其他对象是否与此对象“相等”。
equals方法(在非空对象引用上)实现相等关系:
·
·
·
·
·
Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
equals方法的约定正好描述了它的需求,一旦你完全理解了,正确的实现这个方法将变的非常容易。现在让我们来分析一下每条的真实含义。
1.
2.
3.
4.
5.
6.
public int hashCode()
这个方法将返回调用这个方法的对象的哈希码值。这个方法将返回整数形式的哈希码值,基于散列法的集合将使用这个返回值,例如Hashtable, HashMap, HashSet等。如果equals方法被重写,这个方法也必须被重写。
以下是JDK 1.4 API文档中关于Object类hashCode方法的声明:
返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。
hashCode 的常规协定是:
·
·
·
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
同equals方法的协定相比,hashCode方法的协定相对简单而且容易理解。它声明了在实现hashCode方法时的两个重要条件。第三点协定其实是第二点的详细描述。接下来理解一下这个协定的含义。
1.
2.
在回顾了这两个方法的常用约定之后,可以使用下面的语句清楚地概括两者之间的关系。
相等的对象必须有相同的哈希码,不相等的对象并不一定需要有不同的哈希码。
这两个方法约定中剩余的条约没有直接涉及这两个方法之间的关系。那些条约之前已经讨论过了。这种关系也强调了无论何时你重写equals方法必须重写hashCode方法。如果没有按照这个约定,在使用Java集合类或者其他类的时候常常会导致不确定、不期望的结果。
正确的实现示例
以下的代码展示了如何完全实现equals方法和hashCode方法的约定,使其能够协调地与其他Java类正常工作。这个类的equals方法使用和String等Java内置类型或者包装类相似的方式进行实现:提供了和同类型对象的比较。
- public class Test{
- private int num;
- private String data;
- public boolean equals(Object obj){
- if(this == obj)
- return true;
- if((obj == null) || (obj.getClass() != this.getClass()))
- return false;
- // 此处参数对象为Test类型
- Test test = (Test)obj;
- return num == test.num && (data == test.data || (data != null && data.equals(test.data)));
- }
- public int hashCode(){
- int hash = 7;
- hash = 31 * hash + num;
- hash = 31 * hash + (null == data ? 0 : data.hashCode());
- return hash;
- }
- // 其他方法
- }
现在,解释一下为什么说这个实现是正确的实现。Test类含有两个成员变量: num和data. 这两个变量定义了对象的属性并且参与了这个类的对象的比较。因此,对象哈希码的计算中也应该涉及到这两个变量。
首先来看equals方法。在第8行,我们可以看到被(当作参数)传入的对象和它自己进行比较。如果两个对象的引用指向堆区的同一个对象,在比较操作的代价非常昂贵的条件下这个判断会节省很多时间。接下来,第10行的if条件会判断参数是否为null,如果非null,然后(或操作符||是短路判断)通过比较参数对象的类型和当前对象的类型来检查参数是否是Type类型的。通过调用getClass()来获得当前引用的类型。如果不满足任一条件,方法将返回false。代码如下:
if((obj == null) || (obj.getClass() != this.getClass())) return false; // 推荐
这个条件判断应该是首选,而不是下面的这个:
if(!(obj instanceof Test)) return false; // 避免
这是因为,当参数是Test类的子类,第一种条件表达式判断(蓝色代码的)确保会返回false。然而第二种条件表达式判断(红色代码的)将出错。如果参数是Test类的子类,含有instanceof操作符的判断将不会返回false。因此它可能会违反约定中的对称原则。(译注:instanceof这种比较有可能出现多种情况,一种是不能转型成子类而抛出异常,另一种是父类的private 成员在子类中不能使用因而不能进行比较。)但是如果类是final类型的,则instanceof 的检查是正确的,因为那样这个类将不会有子类。第一种条件表达式检查可以在final类型或者非final类型的类里正常工作。注意,如果参数为 null,这两种条件都回返回false。如果或操作符左边的表达式是null,instanceof操作符会返回false,而不去考虑或操作符右边的表达式。这在Java语言规范JLS 15.20.2中被指定。无论如何,第一种条件表达式检查将是类型检查的首选。
这个类实现的equals方法仅提供了相同类型对象的比较。注意,这不是强制的。但是,如果一个类提供了和其他类型对象比较的方法,那么其他类型的类也同样应该提供和这个类比较的方法,这是为了履行约定中的对称性和自反性原则。equals方法的具体实现不应该违反双方的要求。第14行和第15行实际展示了成员变量data的比较,如果值相同,将返回true。第15行也确保了在执行String类型的equals方法时不会出现空指针异常。
在实现equals方法时,在经过一些必要的转换之后,可以简单的使用 == 操作符进行比较。(例如float和Float.floatToIntBits或者double和Double.doubleToLongBits的比较。) 然而,对象引用之间的比较会引起它们equals方法进行递归。你还需要确保在调用他们的equals方法时不会引发空指针异常。
以下列举了几点正确实现equals方法的指导方针。
1.
2.
if((obj == null) || (obj.getClass() != this.getClass())) return false;
注意, 合适的类型并不意味着类型相同或者是代码示例中的类型。它可以是提供了比较方法的任一类型或接口。
3.
4.
5.
6.
7.
现在,来看看示例中的hashCode方法。在代码的20行,一个非零的常数7(任意的)被赋值给变量hash。因为类的成员变量num和data参与了equals方法中的比较,它们也应该在哈希码的计算中被引入。尽管这并不是强制性的。你也可以使用参与计算equals方法的变量的子集来提高hashCode方法的性能。hashCode方法的性能确实非常重要,但是你也得小心挑选这个子集。这个子集应该包含那些最有可能产生最大差异值的变量。有时候,使用所有参与equals方法的变量将使哈希码的计算结果更有意义。示例代码中的类使用了num和data这两个变量来计算哈希码。代码中的21行和22行就是基于这两个变量来计算哈希码的。第22行也确保了在执行hashCode方法时,如果data 变量为null不会导致空指针异常。这个实现确保了没有违反hashCode方法的约定。这个实现在多次调用hashCode方法时会返回一致的哈希码值,并且保证相等的对象返回相同的哈希码。在实现hashCode方法时,经过必要的转换之后可以直接使用基本类型计算哈希码值。例如float和Float.floatToIntBits或者double和Double.doubleToLongBits。因为hashCode方法必须返回int型,所以long类型的值必须转换为整型。至于引用类型的哈希码,调用这些对象的hashCode方法可以被计算出来。你应该确保在调用引用类型的hashCode方法不会导致空指针异常。写出一个优秀的hashCode方法实现不是一件容易的事情,它计算出来的哈希码应该是均衡分布的,这可能需要数学家和计算机理论科学家的参与。然而,遵循下列简单的规则也可以写出一个相当好的实现。
以下列举了几点正确实现hashCode方法的指导方针。
1.
2.
a.
b.
c.
d.
long bits = Double.doubleToLongBits(var);
var_code = (int)(bits ^ (bits >>> 32));
e.
f.
var_code = (null == var ? 0 : var.hashCode());
3.
hash = 31 * hash + var_code;
4.
5.
这里所提出的方针仅作为指导,并不是绝对的准则。不过遵循这些方针来实现这两个方法的确会产生正确、一致的结果。
摘要与技巧
·
·
·
·
·
·
·
·
·
·
·
·
·
·
复习练习
几个有用的复习题 – 点这
资源列表
如果你想了解更多关于这两个方法的细节,这份列表能给你提供帮助。
·
·
·
·
·
·
·