Equals 方法和 HashCode方法

来源:互联网 发布:短作业优先算法例题 编辑:程序博客网 时间:2024/05/01 12:12

 

原文地址:http://www.geocities.com/technofundo/tech/java/equalhash.html

                    http://blog.163.com/cosion@126/blog/static/3415796420087711534555/

前言

在Java语言中,所有类的父类 java.lang.Object 中有两个非常重要的方法。

·     public boolean equals(Object obj)

·     public int hashCode()

在用户类和其他类进行比较的时候或被添加到集合中的时候,这两个方法的重要性就被体现出来了。这两个方法已经成为了 SCJP 1.4认证的题目。本文将向有志于SCJP 1.4认证的人提供一些关于这两个方法的必要信息,即使你对此认证并无兴趣,也可以帮助你理解这两个方法的机制,并在你自己的类中实现这些方法。

public boolean equals(Object obj)

这个方法是用来判断调用equals方法的对象与被当作参数传入的其他对象是否相等。equals方法在Object类中的默认实现是仅仅是判断两个对象的引用x和y是否指向同一个对象即判断x == y是否成立。这个特殊的比较即“浅比较”。然而,自己实现了equals方法的类应该实现“深比较”,即比较相应的数据内容。因为Object类中没有属性,所以使用“浅比较”做简单实现。

以下是JDK 1.4 API文档中关于Objectequals方法的协定:

指出某个其他对象是否与此对象“相等”。

equals方法(在非空对象引用上)实现相等关系:

·     自反性:对于任何(非空)引用值 x,x.equals(x) 都应返回 true。

·     对称性:对于任何(非空)引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 才应返回 true。

·     传递性:对于任何(非空)引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么 x.equals(z) 应返回 true。

·     一致性:对于任何(非空)引用值 x 和 y,多次调用 x.equals(y) 始终返回 true 或始终返回 false,前提是对象上 equals 比较中所用的信息没有被修改。

·     对于任何非空引用值 x,x.equals(null) 都应返回 false。

Object 类的 equals 方法实现对象上差别可能性最大的相等关系;即,对于任何非空引用值 x 和 y,当且仅当 x 和 y 引用同一个对象时,此方法才返回 true(x == y 具有值 true)。

注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。

equals方法的约定正好描述了它的需求,一旦你完全理解了,正确的实现这个方法将变的非常容易。现在让我们来分析一下每条的真实含义。

1.  自反性 - 它仅仅描述了在任何情况下,一个对象必须等于它本身。除非你故意以不同的方式实现equals方法。

2.  对称性 - 如果一个类的对象与另一个类的对象相等,那么另一个类的对象也必须等于这个类的对象。换句话说,一个对象不能单方面决定它和另一个对象相等。因此,属于两个类的两个对象,必须从两方面判断他们是否相等。这两者必须一致。因此,使用你自己类中的equals方法与java.lang.String类或者其他Java内置类的对象进行比较是不恰当且不正确的。完全理解这点是非常重要的,因为对equals方法的一个不恰当实现很有可能违背这条规则并导致一个非期望的结果。

3.  传递性 - 第一个对象等于第二个对象并且第二个对象等于第三个对象,那么第一个对象等于第三个对象。换而言之,如果两个对象都相等,根据对称原则,它们中的一个对象不能决定它等于不同类型的另一个对象。所有的这三者不同的排列组合必须同时遵循对称原则。看一下这个例子,A,B,C是三个类。A和B都以这样的一种方式实现了equals方法:比较A类和B类对象的方法。现在,如果B类的作者决定修改他的equals方法,这个新实现的方法同样可以比较C类,那么他将违反传递原则。因为,在A类和C类的对象里没有存在合适的比较方法。

4.  一致性 – 如果两个对象相等,只要它们没有被修改,那么它们必须保持这种相等关系。同样地,如果他们不相等,只要它们没有被修改,那么它们也将保持这种不等关系。这种修改可能发生在它们中的任一个或两者都被修改。

5.  与null比较 – 任何一个类的对象不等于null,因此当null被作为equals方法的参数传入时,equals方法必须返回false。你得确保当null被作为equals方法的参数传入时,你所实现的equals方法应该返回false。

6.  Equals和哈希码的关系 – API文档最后的注意事项非常重要,它描述了这两个方法之间的关系。它说明了如果两个对象相等,那么他们必须有同样的哈希码。然而,逆命题不成立。关于这点在文章的后面会有详细讨论。

public int hashCode()

这个方法将返回调用这个方法的对象的哈希码值。这个方法将返回整数形式的哈希码值,基于散列法的集合将使用这个返回值,例如Hashtable, HashMap, HashSet等。如果equals方法被重写,这个方法也必须被重写。

以下是JDK 1.4 API文档中关于ObjecthashCode方法的声明:

返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。

hashCode 的常规协定是:

·     在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。

·     如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。

·     以下情况不是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。

实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)

equals方法的协定相比,hashCode方法的协定相对简单而且容易理解。它声明了在实现hashCode方法时的两个重要条件。第三点协定其实是第二点的详细描述。接下来理解一下这个协定的含义。

1.  同一次执行中的一致性 - 首先,协定声明了hashCode方法返回的哈希码在应用程序的同一次执行中的多次调用必须是始终一样的,只要对象中影响equals方法的信息没有被修改。

2.  哈希码和Equals的关系 – 协定中的第二点是说hashCode方法是equals方法的必要条件。它仅仅强调了同样的关系——相等的对象必须含有相同的哈希码。第三点中详细描述了不相等的对象并不一定需要有不同的哈希码。

在回顾了这两个方法的常用约定之后,可以使用下面的语句清楚地概括两者之间的关系。

相等的对象必须有相同的哈希码,不相等的对象并不一定需要有不同的哈希码。

 

这两个方法约定中剩余的条约没有直接涉及这两个方法之间的关系。那些条约之前已经讨论过了。这种关系也强调了无论何时你重写equals方法必须重写hashCode方法。如果没有按照这个约定,在使用Java集合类或者其他类的时候常常会导致不确定、不期望的结果。

正确的实现示例

以下的代码展示了如何完全实现equals方法和hashCode方法的约定,使其能够协调地与其他Java类正常工作。这个类的equals方法使用和String等Java内置类型或者包装类相似的方式进行实现:提供了和同类型对象的比较。

  1. public class Test{

  2.     private int num;

  3.     private String data;  

  4.     public boolean equals(Object obj){
  5.         if(this == obj)
  6.             return true;
  7.         if((obj == null) || (obj.getClass() != this.getClass()))
  8.             return false;
  9.         // 此处参数对象为Test类型
  10.         Test test = (Test)obj;
  11.         return num == test.num && (data == test.data || (data != null && data.equals(test.data)));
  12.     }

  13.     public int hashCode(){
  14.         int hash = 7;
  15.         hash = 31 * hash + num;
  16.         hash = 31 * hash + (null == data ? 0 : data.hashCode());
  17.         return hash;
  18.     }

  19.     // 其他方法
  20. } 

现在,解释一下为什么说这个实现是正确的实现。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.  如果参数为引用类型,使用 == 操作符进行比较。如果相等返回true. 在比较的代价很昂贵时这样做将节省时间。

2.  使用下列的条件表达式判断参数是否为null,类型是否合适。条件不满足将返回false。
if((obj == null) || (obj.getClass() != this.getClass())) return false;
注意, 合适的类型并不意味着类型相同或者是代码示例中的类型。它可以是提供了比较方法的任一类型或接口。

3.  将参数类型转换为合适的类型。再提一句,合适的类型可能并不是类型相同的。同样,因为上面的类型检查表达式,这步不会发生类型转换异常。

4.  比较参数对象和当前对象中重要的变量是否相等,如果全部条件都满足将返回true,否则返回false。正如之前提到的,当比较类的成员或变量时,基本类型的变量可以直接使用 == 操作符与经过必要转换的变量进行比较(例如float和 Float.floatToIntBits或double to Double.doubleToLongBits)。然而,对象引用之间的比较会引起它们equals方法进行递归。你还需要确保在调用他们的equals方法时不会引发空指针异常。正如实例代码中15行所做的那样。在比较中包含那些从其他变量计算出来的成员变量既不是必须的也不是明智的做法。这保证了equals方法的性能。仅仅由你决定类的成员中哪些是重要的哪些不是。

5.  不要改变equals方法的参数类型,它必须使用java.lang.Object作为参数类型,而不要使用你自定义的类型。如果你改变了参数类型,将不是重写equals方法而是重载了这个方法。这个常见的错误会导致很多问题。因为这不是一个编译错误,在你的代码不能正常运行的时候会使你很难找到问题的解决办法。

6.  回顾你的equals方法检查它是否全部实现了equals方法的全部约定。

7.  最后,当你重写了equals方法不要忘记重写hashCode方法,这可是不可原谅的错误。;)

现在,来看看示例中的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.  为整型变量hash设定任意一个非零的整数(比方说 7)。

2.  在计算对象的哈希码的时候,需要包含参与计算equals方法的变量。为每个变量var计算一个单独的哈希码var_code,应该遵循以下规则 -

a.  如果变量var是 byte,char,short或int类型,那么var_code = (int)var;

b.  如果变量var是long类型,那么var_code = (int)(var ^ (var >>> 32));

c.  如果变量var是float类型,那么var_code = Float.floatToIntBits(var);

d.  如果变量var是double类型,那么
long bits = Double.doubleToLongBits(var);
var_code = (int)(bits ^ (bits >>> 32));

e.  如果变量var是boolean,那么 var_code = var ? 1 : 0;

f.  如果变量var是引用类型,首先检查它是否为null,如果为null,var_code = 0; 否则执行引用类型的hashCode方法获取哈希码。简单实例代码如下:
var_code = (null == var ? 0 : var.hashCode());

3.  联合这些单独变量的哈希码var_code与原始哈希码变量hash做如下运算:
hash = 31 * hash + var_code;

4.  按照以上这些步骤计算对象中重要变量的哈希码值最后返回整型的计算结果hash

5.  最后,检查你的hashCode方法并确认相等对象是否会返回相同的哈希码值。并且验证同一次运行中在同一对象上多次调用多次调用hashCode方法返回的哈希码值是否相同。

这里所提出的方针仅作为指导,并不是绝对的准则。不过遵循这些方针来实现这两个方法的确会产生正确、一致的结果。

摘要与技巧

·     只要两个对象相等,那么这两个相等的对象应该含有相同的哈希码。然而,不相等的对象不一定要有不同的哈希码。

·     和 == 操作符的“浅比较”不同的是equals方法通过检查两个对象是否在逻辑上相等的方法来实现“深比较”。

·     在java.lang.Object 类中equals方法仍然只提供了和 == 操作符相同的“浅比较”。

·     equals方法的使用了Java对象类型作为参数而不是基本类型。调用equals方法时使用一个基本类型将会导致一个编译错误。

·     在调用equals方法时传入不同的引用类型不会导致编译错误或运行时错误。

·     对于java.lang.String类或标准的Java包装类,如果使用equals方法时,参数类型和调用equals方法对象的类型不同,会返回false。

·     java.lang.StringBuffer 类没有覆盖java.lang.Object 类的equals方法,它继承了java.lang.Object 类的实现。

·     你实现的equals方法不能和任何Java内置类型进行比较,因为那样会违反equals方法约定中的对称性原则。

·     如果equals方法传入的参数为null,返回值为false。

·     具有相同哈希码值的对象并不意味着这两个对象相等。

·     return 1; 这是一个合法的hashCode方法实现,然而这是一种非常不推荐的实现。之所以说它合法,仅仅是因为它确保相等的对象含有相同的哈希码,确保在同一次运行中的多次调用会返回一致的哈希码。因而它没有违反hashCode方法的常规约定。之所以说它是不好的实现,因为它为所有对象返回相同的哈希码。这个说明适用与所有返回相同整型值的hashCode方法实现。

·     在标准的JDK 1.4版本中,如下的几个包装类java.lang.Short, java.lang.Byte, java.lang.Character 和java.lang.Integer 类只是简单以它们对应的整型值作为哈希码返回。

·     从JDK 1.3 版本开始,java.lang.String 类缓存了它的哈希码,它只计算一次哈希码并且把它存入一个整形变量中,下次调用hashCode方法时,它将直接被返回。这样做是合法的,因为java.lang.String 被设计为不可变的。

·     在计算一个对象的哈希码值时包含一个随机数是不正确的,因为它在同一次运行中的多次调用不会返回一致的哈希码。

复习练习

几个有用的复习题 – 点这

资源列表

如果你想了解更多关于这两个方法的细节,这份列表能给你提供帮助。

·     Object class - java.lang.Object 类的API文档。这两个方法的常用约定可以在这里找到。

·     Effective Java - Joshua Bloch的一本好书。这本书pdf格式的第三章可以在线获取。这章涉及了java.lang.Object 类所有的方法。它也详细讨论了equals方法和hashCode方法的实现,正确和不正确的方法等。本文部分内容来源与这本书所提供的信息。然而,本文试图通过一些简洁的例子来解释这两个方法的机制和实现细节。并且提供了一些有助于理解的技巧和复习题。

·     JavaWorld Article – 这篇文章讨论了java.lang.Object 类的相关方法,也解释了在equals方法中使用 instanceof 条件表达式的缺点。

·     Importance of equals and hashCode – 这个常见问题解答中谈到了正确覆盖equals方法和hashCode方法的重要性。它也提到了这两个方法在Java集合框架中的重要地位。

·     Equals & Hash Code Mock Test – 如果你对这两个方法的小测试感兴趣的话。

·     Equals and Hash Code Cartoon – 我创作的一个连环画,尝试使用插图的方式解释equals方法和hashCode方法的关系。

·     Whizlabs Java Certification (SCJP 1.4) Exam Simulator – 详细解释了关于equals方法和hashCode方法的题目,还有其他的一些SCJP 1.4 试题。

 

0 0
原创粉丝点击