重写equal()时为什么也得重写hashCode()之深度解读以及equal方法与hashCode方法渊源
转载自:http://blog.csdn.net/javazejian/article/details/51348320
今天这篇文章我们打算来深度解读一下equal方法以及其关联方法hashCode(),我们准备从以下几点入手分析:
1.equals()的所属以及内部原理(即Object中equals方法的实现原理)
说起equals方法,我们都知道是超类Object中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用,如果有,它们就一定相等。其源码如下:
- public boolean equals(Object obj) { return (this == obj); }
实际上我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等,即若 object1.equals(object2) 为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。
2.equals()与‘==’的区别
或许这是我们面试时更容易碰到的问题”equals方法与‘==’运算符有什么区别?“,并且常常我们都会胸有成竹地回答:“equals比较的是对象的内容,而‘==’比较的是对象的地址。”。但是从前面我们可以知道equals方法在Object中的实现也是间接使用了‘==’运算符进行比较的,所以从严格意义上来说,我们前面的回答并不完全正确。我们先来看一段代码并运行再来讨论这个问题。
- package com.zejian.test;
- public class Car {
- private int batch;
- public Car(int batch) {
- this.batch = batch;
- }
- public static void main(String[] args) {
- Car c1 = new Car(1);
- Car c2 = new Car(1);
- System.out.println(c1.equals(c2));
- System.out.println(c1 == c2);
- }
- }
运行结果:
false false |
分析:对于‘==’运算符比较两个Car对象,返回了false,这点我们很容易明白,毕竟它们比较的是内存地址,而c1与c2是两个不同的对象,所以c1与c2的内存地址自然也不一样。现在的问题是,我们希望生产的两辆的批次(batch)相同的情况下就认为这两辆车相等,但是运行的结果是尽管c1与c2的批次相同,但equals的结果却反回了false。当然对于equals返回了false,我们也是心知肚明的,因为equal来自Object超类,访问修饰符为public,而我们并没有重写equal方法,故调用的必然是Object超类的原始方equals方法,根据前面分析我们也知道该原始equal方法内部实现使用的是'=='运算符,所以返回了false。因此为了达到我们的期望值,我们必须重写Car的equal方法,让其比较的是对象的批次(即对象的内容),而不是比较内存地址,于是修改如下:
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof Car) {
- Car c = (Car) obj;
- return batch == c.batch;
- }
- return false;
- }
使用instanceof来判断引用obj所指向的对象的类型,如果obj是Car类对象,就可以将其强制转为Car对象,然后比较两辆Car的批次,相等返回true,否则返回false。当然如果obj不是 Car对象,自然也得返回false。我们再次运行:
true false |
嗯,达到我们预期的结果了。因为前面的面试题我们应该这样回答更佳
总结:默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址,但我们可以重写equals方法,使其按照我们的需求的方式进行比较,如String类重写了equals方法,使其比较的是字符的序列,而不再是内存地址。
3.equals()的重写规则
前面我们已经知道如何去重写equals方法来实现我们自己的需求了,但是我们在重写equals方法时,还是需要注意如下几点规则的。
自反性。对于任何非null的引用值x,x.equals(x)应返回true。
对称性。对于任何非null的引用值x与y,当且仅当:y.equals(x)返回true时,x.equals(y)才返回true。
传递性。对于任何非null的引用值x、y与z,如果y.equals(x)返回true,y.equals(z)返回true,那么x.equals(z)也应返回true。
一致性。对于任何非null的引用值x与y,假设对象上equals比较中的信息没有被修改,则多次调用x.equals(y)始终返回true或者始终返回false。
对于任何非空引用值x,x.equal(null)应返回false。
当然在通常情况下,如果只是进行同一个类两个对象的相等比较,一般都可以满足以上5点要求,下面我们来看前面写的一个例子。
- package com.zejian.test;
- public class Car {
- private int batch;
- public Car(int batch) {
- this.batch = batch;
- }
- public static void main(String[] args) {
- Car c1 = new Car(1);
- Car c2 = new Car(1);
- Car c3 = new Car(1);
- System.out.println("自反性->c1.equals(c1):" + c1.equals(c1));
- System.out.println("对称性:");
- System.out.println(c1.equals(c2));
- System.out.println(c2.equals(c1));
- System.out.println("传递性:");
- System.out.println(c1.equals(c2));
- System.out.println(c2.equals(c3));
- System.out.println(c1.equals(c3));
- System.out.println("一致性:");
- for (int i = 0; i < 50; i++) {
- if (c1.equals(c2) != c1.equals(c2)) {
- System.out.println("equals方法没有遵守一致性!");
- break;
- }
- }
- System.out.println("equals方法遵守一致性!");
- System.out.println("与null比较:");
- System.out.println(c1.equals(null));
- }
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof Car) {
- Car c = (Car) obj;
- return batch == c.batch;
- }
- return false;
- }
- }
运行结果:
自反性->c1.equals(c1):true 对称性: true true 传递性: true true true 一致性: equals方法遵守一致性! 与null比较: false |
由运行结果我们可以看出equals方法在同一个类的两个对象间的比较还是相当容易理解的。但是如果是子类与父类混合比较,那么情况就不太简单了。下面我们来看看另一个例子,首先,我们先创建一个新类BigCar,继承于Car,然后进行子类与父类间的比较。
- package com.zejian.test;
- public class BigCar extends Car {
- int count;
- public BigCar(int batch, int count) {
- super(batch);
- this.count = count;
- }
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof BigCar) {
- BigCar bc = (BigCar) obj;
- return super.equals(bc) && count == bc.count;
- }
- return false;
- }
- public static void main(String[] args) {
- Car c = new Car(1);
- BigCar bc = new BigCar(1, 20);
- System.out.println(c.equals(bc));
- System.out.println(bc.equals(c));
- }
- }
运行结果:
true false |
对于这样的结果,自然是我们意料之中的啦。因为BigCar类型肯定是属于Car类型,所以c.equals(bc)肯定为true,对于bc.equals(c)返回false,是因为Car类型并不一定是BigCar类型(Car类还可以有其他子类)。嗯,确实是这样。但如果有这样一个需求,只要BigCar和Car的生产批次一样,我们就认为它们两个是相当的,在这样一种需求的情况下,父类(Car)与子类(BigCar)的混合比较就不符合equals方法对称性特性了。很明显一个返回true,一个返回了false,根据对称性的特性,此时两次比较都应该返回true才对。那么该如何修改才能符合对称性呢?其实造成不符合对称性特性的原因很明显,那就是因为Car类型并不一定是BigCar类型(Car类还可以有其他子类),在这样的情况下(Car instanceof BigCar)永远返回false,因此,我们不应该直接返回false,而应该继续使用父类的equals方法进行比较才行(因为我们的需求是批次相同,两个对象就相等,父类equals方法比较的就是batch是否相同)。因此BigCar的equals方法应该做如下修改:
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof BigCar) {
- BigCar bc = (BigCar) obj;
- return super.equals(bc) && count == bc.count;
- }
- return super.equals(obj);
- }
- package com.zejian.test;
- public class BigCar extends Car {
- int count;
- public BigCar(int batch, int count) {
- super(batch);
- this.count = count;
- }
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof BigCar) {
- BigCar bc = (BigCar) obj;
- return super.equals(bc) && count == bc.count;
- }
- return super.equals(obj);
- }
- public static void main(String[] args) {
- Car c = new Car(1);
- BigCar bc = new BigCar(1, 20);
- BigCar bc2 = new BigCar(1, 22);
- System.out.println(bc.equals(c));
- System.out.println(c.equals(bc2));
- System.out.println(bc.equals(bc2));
- }
- }
运行结果:
true true false |
bc,bc2,c的批次都是相同的,按我们之前的需求应该是相等,而且也应该符合equals的传递性才对。但是事实上运行结果却不是这样,违背了传递性。出现这种情况根本原因在于:
父类与子类进行混合比较。
子类中声明了新变量,并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。
只要满足上面两个条件,equals方法的传递性便失效了。而且目前并没有直接的方法可以解决这个问题。因此我们在重写equals方法时这一点需要特别注意。虽然没有直接的解决方法,但是间接的解决方案还说有滴,那就是通过组合的方式来代替继承,还有一点要注意的是组合的方式并非真正意义上的解决问题(只是让它们间的比较都返回了false,从而不违背传递性,然而并没有实现我们上面batch相同对象就相等的需求),而是让equals方法满足各种特性的前提下,让代码看起来更加合情合理,代码如下:
- package com.zejian.test;
- public class Combination4BigCar {
- private Car c;
- private int count;
- public Combination4BigCar(int batch, int count) {
- c = new Car(batch);
- this.count = count;
- }
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof Combination4BigCar) {
- Combination4BigCar bc = (Combination4BigCar) obj;
- return c.equals(bc.c) && count == bc.count;
- }
- return false;
- }
- }
从代码来看即使batch相同,Combination4BigCar类的对象与Car类的对象间的比较也永远都是false,但是这样看起来也就合情合理了,毕竟Combination4BigCar也不是Car的子类,因此equals方法也就没必要提供任何对Car的比较支持,同时也不会违背了equals方法的传递性。
4.equals()的重写规则之必要性深入解读
前面我们一再强调了equals方法重写必须遵守的规则,接下来我们就是分析一个反面的例子,看看不遵守这些规则到底会造成什么样的后果。
- package com.zejian.test;
- import java.util.ArrayList;
- import java.util.List;
- /** * 反面例子 * @author zejian */
- public class AbnormalResult {
- public static void main(String[] args) {
- List<A> list = new ArrayList<A>();
- A a = new A();
- B b = new B();
- list.add(a);
- System.out.println("list.contains(a)->" + list.contains(a));
- System.out.println("list.contains(b)->" + list.contains(b));
- list.clear();
- list.add(b);
- System.out.println("list.contains(a)->" + list.contains(a));
- System.out.println("list.contains(b)->" + list.contains(b));
- }
- static class A {
- @Override
- public boolean equals(Object obj) {
- return obj instanceof A;
- }
- }
- static class B extends A {
- @Override
- public boolean equals(Object obj) {
- return obj instanceof B;
- }
- }
- }
上面的代码,我们声明了 A,B两个类,注意必须是static,否则无法被main调用。B类继承A,两个类都重写了equals方法,但是根据我们前面的分析,这样重写是没有遵守对称性原则的,我们先来看看运行结果:
list.contains(a)->true list.contains(b)->false list.contains(a)->true list.contains(b)->true |
19行和24行的输出没什么好说的,将a,b分别加入list中,list中自然会含有a,b。但是为什么20行和23行结果会不一样呢?我们先来看看contains方法内部实现
- @Override
- public boolean contains(Object o) {
- return indexOf(o) != -1;
- }
- @Override
- ublic int indexOf(Object o) {
- E[] a = this.a;
- if (o == null) {
- for (int i = 0; i < a.length; i++)
- if (a[i] == null)
- return i;
- } else {
- for (int i = 0; i < a.length; i++)
- if (o.equals(a[i]))
- return i;
- }
- return -1;
可以看出最终调用的是对象的equals方法,所以当调用20行代码list.contains(b)时,实际上调用了
b.equals(a[i]),a[i]是集合中的元素集合中的类型而且为A类型(只添加了a对象),虽然B继承了A,但此时
- a[i] instanceof B
- a[i] instanceof A
- static class B extends A{
- @Override
- public boolean equals(Object obj) {
- if(obj instanceof B){
- return true;
- }
- return super.equals(obj);
- }
- }
到此,我们也应该明白了重写equals必须遵守几点原则的重要性了。当然这里不止是list,只要是java集合类或者java类库中的其他方法,重写equals不遵守5点原则的话,都可能出现意想不到的结果。
5.为什么重写equals()的同时还得重写hashCode()
这个问题之前我也很好奇,不过最后还是在书上得到了比较明朗的解释,当然这个问题主要是针对映射相关的操作(Map接口)。学过数据结构的同学都知道Map接口的类会使用到键对象的哈希码,当我们调用put方法或者get方法对Map容器进行操作时,都是根据键对象的哈希码来计算存储位置的,因此如果我们对哈希码的获取没有相关保证,就可能会得不到预期的结果。在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。那我们先来认识一下hashCode()这个方法吧。hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的,如果x与y是两个不同的对象,那么x.hashCode()与y.hashCode()基本是不会相同的,下面通过String类的hashCode()计算一组散列码:
- package com.zejian.test;
- public class HashCodeTest {
- public static void main(String[] args) {
- int hash=0;
- String s="ok";
- StringBuilder sb =new StringBuilder(s);
- System.out.println(s.hashCode()+" "+sb.hashCode());
- String t = new String("ok");
- StringBuilder tb =new StringBuilder(s);
- System.out.println(t.hashCode()+" "+tb.hashCode());
- }
- }
运行结果:
3548 1829164700 3548 2018699554 |
我们可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址,所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢,其实并不难,我们只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:
- package com.zejian.test;
- public class Model {
- private String name;
- private double salary;
- private int sex;
- @Override
- public int hashCode() {
- return name.hashCode()+new Double(salary).hashCode()
- + new Integer(sex).hashCode();
- }
- }
- public static int hashCode(Object o) {
- return o != null ? o.hashCode() : 0;
- }
- package com.zejian.test;
- import java.util.Objects;
- public class Model {
- private String name;
- private double salary;
- private int sex;
- @Override
- public int hashCode() {
- return Objects.hashCode(name)+new Double(salary).hashCode()
- + new Integer(sex).hashCode();
- }
- }
- package com.zejian.test;
- import java.util.Objects;
- public class Model {
- private String name;
- private double salary;
- private int sex;
- // @Override
- // public int hashCode() {
- // return Objects.hashCode(name)+new Double(salary).hashCode()
- // + new Integer(sex).hashCode();
- // }
- @Override
- public int hashCode() {
- return Objects.hash(name,salary,sex);
- }
- }
好了,到此hashCode()该介绍的我们都说了,还有一点要说的如果我们提供的是一个数值类型的变量的话,那么我们可以调用Arrays.hashCode()来计算它的散列码,这个散列码是由数组元素的散列码组成的。接下来我们回归到我们之前的问题,重写equals方法时也必须重写hashCode方法。在Java API文档中关于hashCode方法有以下几点规定(原文来自java深入解析一书)。
在java应用程序执行期间,如果在equals方法比较中所用的信息没有被修改,那么在同一个对象上多次调用hashCode方法时必须一致地返回相同的整数。如果多次执行同一个应用时,不要求该整数必须相同。
如果两个对象通过调用equals方法是相等的,那么这两个对象调用hashCode方法必须返回相同的整数。
如果两个对象通过调用equals方法是不相等的,不要求这两个对象调用hashCode方法必须返回不同的整数。但是程序员应该意识到对不同的对象产生不同的hash值可以提供哈希表的性能。
通过前面的分析,我们知道在Object类中,hashCode方法是通过Object对象的地址计算出来的,因为Object对象只与自身相等,所以同一个对象的地址总是相等的,计算取得的哈希码也必然相等,对于不同的对象,由于地址不同,所获取的哈希码自然也不会相等。因此到这里我们就明白了,如果一个类重写了equals方法,但没有重写hashCode方法,将会直接违法了第2条规定,这样的话,如果我们通过映射表(Map接口)操作相关对象时,就无法达到我们预期想要的效果。如果大家不相信, 可以看看下面的例子(来自java深入解析一书)
- package com.zejian.test;
- import java.util.HashMap;
- import java.util.Map;
- public class MapTest {
- public static void main(String[] args) {
- Map<String,Value> map1 = new HashMap<String,Value>();
- String s1 = new String("key");
- String s2 = new String("key");
- Value value = new Value(2);
- map1.put(s1, value);
- System.out.println("s1.equals(s2):"+s1.equals(s2));
- System.out.println("map1.get(s1):"+map1.get(s1));
- System.out.println("map1.get(s2):"+map1.get(s2));
- Map<Key,Value> map2 = new HashMap<Key,Value>();
- Key k1 = new Key("A");
- Key k2 = new Key("A");
- map2.put(k1, value);
- System.out.println("k1.equals(k2):"+s1.equals(s2));
- System.out.println("map2.get(k1):"+map2.get(k1));
- System.out.println("map2.get(k2):"+map2.get(k2));
- }
- /**
- * 键
- * @author zejian
- *
- */
- static class Key{
- private String k;
- public Key(String key){
- this.k=key;
- }
- @Override
- public boolean equals(Object obj) {
- if(obj instanceof Key){
- Key key=(Key)obj;
- return k.equals(key.k);
- }
- return false;
- }
- }
- /**
- * 值
- * @author zejian
- *
- */
- static class Value{
- private int v;
- public Value(int v){
- this.v=v;
- }
- @Override
- public String toString() {
- return "类Value的值-->"+v;
- }
- }
- }
- s1.equals(s2):true
- map1.get(s1):类Value的值-->2
- map1.get(s2):类Value的值-->2
- k1.equals(k2):true
- map2.get(k1):类Value的值-->2
- map2.get(k2):null
- @Override
- public int hashCode() {
- return k.hashCode();
- }
- s1.equals(s2):true
- map1.get(s1):类Value的值-->2
- map1.get(s2):类Value的值-->2
- k1.equals(k2):true
- map2.get(k1):类Value的值-->2
- map2.get(k2):类Value的值-->2
6.重写equals()中getClass与instanceof的区别
虽然前面我们都在使用instanceof(当然前面我们是根据需求(批次相同即相等)而使用instanceof的),但是在重写equals() 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用instanceof),不是使用 instanceof。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下来我们来看一个例子:父类Person
- public class Person {
- protected String name;
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Person(String name){
- this.name = name;
- }
- public boolean equals(Object object){
- if(object instanceof Person){
- Person p = (Person) object;
- if(p.getName() == null || name == null){
- return false;
- }
- else{
- return name.equalsIgnoreCase(p.getName ());
- }
- }
- return false;
- }
- }
- public class Employee extends Person{
- private int id;
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- public Employee(String name,int id){
- super(name);
- this.id = id;
- }
- /**
- * 重写equals()方法
- */
- public boolean equals(Object object){
- if(object instanceof Employee){
- Employee e = (Employee) object;
- return super.equals(object) && e.getId() == id;
- }
- return false;
- }
- }
- public class Test {
- public static void main(String[] args) {
- Employee e1 = new Employee("chenssy", 23);
- Employee e2 = new Employee("chenssy", 24);
- Person p1 = new Person("chenssy");
- System.out.println(p1.equals(e1));
- System.out.println(p1.equals(e2));
- System.out.println(e1.equals(e2));
- }
- }
7.编写一个完美equals()的几点建议
下面给出编写一个完美的equals方法的建议(出自Java核心技术 第一卷:基础知识):
1)显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量(参数名命名,强制转换请参考建议5)
2)检测this与otherObject是否引用同一个对象 :if(this == otherObject) return true;(存储地址相同,肯定是同个对象,直接返回true)
3) 检测otherObject是否为null ,如果为null,返回false.if(otherObject == null) return false;
4) 比较this与otherObject是否属于同一个类 (视需求而选择)
如果equals的语义在每个子类中有所改变,就使用getClass检测 :if(getClass()!=otherObject.getClass()) return false; (参考前面分析的第6点)
如果所有的子类都拥有统一的语义,就使用instanceof检测 :if(!(otherObject instanceof ClassName)) return false;(即前面我们所分析的父类car与子类bigCar混合比,我们统一了批次相同即相等)
5) 将otherObject转换为相应的类类型变量:ClassName other = (ClassName) otherObject;
6) 现在开始对所有需要比较的域进行比较 。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true,否则就返回flase。
如果在子类中重新定义equals,就要在其中包含调用super.equals(other)
当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明 相等对象必须具有相等的哈希码 。