重写equal()时为什么也得重写hashCode()之深度解读equal方法与hashCode方法渊源

重写equal()时为什么也得重写hashCode()之深度解读以及equal方法与hashCode方法渊源



转载自:http://blog.csdn.net/javazejian/article/details/51348320

今天这篇文章我们打算来深度解读一下equal方法以及其关联方法hashCode(),我们准备从以下几点入手分析:


1.equals()的所属以及内部原理(即Object中equals方法的实现原理)

说起equals方法,我们都知道是超类Object中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用,如果有,它们就一定相等。其源码如下:

[java] view plaincopy
  1. public boolean equals(Object obj) {   return (this == obj);     }  

实际上我们知道所有的对象都拥有标识(内存地址)和状态(数据),同时“==”比较两个对象的的内存地址,所以说 Object 的 equals() 方法是比较两个对象的内存地址是否相等,即若 object1.equals(object2) 为 true,则表示 equals1 和 equals2 实际上是引用同一个对象。

2.equals()与‘==’的区别

或许这是我们面试时更容易碰到的问题”equals方法与‘==’运算符有什么区别?“,并且常常我们都会胸有成竹地回答:“equals比较的是对象的内容,而‘==’比较的是对象的地址。”。但是从前面我们可以知道equals方法在Object中的实现也是间接使用了‘==’运算符进行比较的,所以从严格意义上来说,我们前面的回答并不完全正确。我们先来看一段代码并运行再来讨论这个问题。

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class Car {  
  3.     private int batch;  
  4.     public Car(int batch) {  
  5.         this.batch = batch;  
  6.     }  
  7.     public static void main(String[] args) {  
  8.         Car c1 = new Car(1);  
  9.         Car c2 = new Car(1);  
  10.         System.out.println(c1.equals(c2));  
  11.         System.out.println(c1 == c2);  
  12.     }  
  13. }  

运行结果:

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方法,让其比较的是对象的批次(即对象的内容),而不是比较内存地址,于是修改如下:

[java] view plaincopy
  1. @Override  
  2.     public boolean equals(Object obj) {  
  3.         if (obj instanceof Car) {  
  4.             Car c = (Car) obj;  
  5.             return batch == c.batch;  
  6.         }  
  7.         return false;  
  8.     }  

使用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点要求,下面我们来看前面写的一个例子。

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class Car {  
  3.     private int batch;  
  4.     public Car(int batch) {  
  5.         this.batch = batch;  
  6.     }  
  7.     public static void main(String[] args) {  
  8.         Car c1 = new Car(1);  
  9.         Car c2 = new Car(1);  
  10.         Car c3 = new Car(1);  
  11.         System.out.println("自反性->c1.equals(c1):" + c1.equals(c1));  
  12.         System.out.println("对称性:");  
  13.         System.out.println(c1.equals(c2));  
  14.         System.out.println(c2.equals(c1));  
  15.         System.out.println("传递性:");  
  16.         System.out.println(c1.equals(c2));  
  17.         System.out.println(c2.equals(c3));  
  18.         System.out.println(c1.equals(c3));  
  19.         System.out.println("一致性:");  
  20.         for (int i = 0; i < 50; i++) {  
  21.             if (c1.equals(c2) != c1.equals(c2)) {  
  22.                 System.out.println("equals方法没有遵守一致性!");  
  23.                 break;  
  24.             }  
  25.         }  
  26.         System.out.println("equals方法遵守一致性!");  
  27.         System.out.println("与null比较:");  
  28.         System.out.println(c1.equals(null));  
  29.     }  
  30.     @Override  
  31.     public boolean equals(Object obj) {  
  32.         if (obj instanceof Car) {  
  33.             Car c = (Car) obj;  
  34.             return batch == c.batch;  
  35.         }  
  36.         return false;  
  37.     }  
  38. }  

运行结果:

自反性->c1.equals(c1):true

对称性:

true

true

传递性:

true

true

true

一致性:

equals方法遵守一致性!

与null比较:

false

由运行结果我们可以看出equals方法在同一个类的两个对象间的比较还是相当容易理解的。但是如果是子类与父类混合比较,那么情况就不太简单了。下面我们来看看另一个例子,首先,我们先创建一个新类BigCar,继承于Car,然后进行子类与父类间的比较。

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class BigCar extends Car {  
  3.     int count;  
  4.     public BigCar(int batch, int count) {  
  5.         super(batch);  
  6.         this.count = count;  
  7.     }  
  8.     @Override  
  9.     public boolean equals(Object obj) {  
  10.         if (obj instanceof BigCar) {  
  11.             BigCar bc = (BigCar) obj;  
  12.             return super.equals(bc) && count == bc.count;  
  13.         }  
  14.         return false;  
  15.     }  
  16.     public static void main(String[] args) {  
  17.         Car c = new Car(1);  
  18.         BigCar bc = new BigCar(120);  
  19.         System.out.println(c.equals(bc));  
  20.         System.out.println(bc.equals(c));  
  21.     }  
  22. }  

运行结果:

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方法应该做如下修改:

[java] view plaincopy
  1. @Override  
  2. public boolean equals(Object obj) {  
  3.     if (obj instanceof BigCar) {  
  4.         BigCar bc = (BigCar) obj;  
  5.         return super.equals(bc) && count == bc.count;  
  6.     }  
  7.     return super.equals(obj);  
  8. }  
这样运行的结果就都为true了。但是到这里问题并没有结束,虽然符合了 对称性,却还没符合传递性 ,实例如下:
[java] view plaincopy
  1. package com.zejian.test;  
  2. public class BigCar extends Car {  
  3.     int count;  
  4.     public BigCar(int batch, int count) {  
  5.         super(batch);  
  6.         this.count = count;  
  7.     }  
  8.     @Override  
  9.     public boolean equals(Object obj) {  
  10.         if (obj instanceof BigCar) {  
  11.             BigCar bc = (BigCar) obj;  
  12.             return super.equals(bc) && count == bc.count;  
  13.         }  
  14.         return super.equals(obj);  
  15.     }  
  16.     public static void main(String[] args) {  
  17.         Car c = new Car(1);  
  18.         BigCar bc = new BigCar(120);  
  19.         BigCar bc2 = new BigCar(122);  
  20.         System.out.println(bc.equals(c));  
  21.         System.out.println(c.equals(bc2));  
  22.         System.out.println(bc.equals(bc2));  
  23.     }  
  24. }  

运行结果:

true

true

false

bc,bc2,c的批次都是相同的,按我们之前的需求应该是相等,而且也应该符合equals的传递性才对。但是事实上运行结果却不是这样,违背了传递性。出现这种情况根本原因在于:

  • 父类与子类进行混合比较。

  • 子类中声明了新变量,并且在子类equals方法使用了新增的成员变量作为判断对象是否相等的条件。

只要满足上面两个条件,equals方法的传递性便失效了。而且目前并没有直接的方法可以解决这个问题。因此我们在重写equals方法时这一点需要特别注意。虽然没有直接的解决方法,但是间接的解决方案还说有滴,那就是通过组合的方式来代替继承,还有一点要注意的是组合的方式并非真正意义上的解决问题(只是让它们间的比较都返回了false,从而不违背传递性,然而并没有实现我们上面batch相同对象就相等的需求),而是让equals方法满足各种特性的前提下,让代码看起来更加合情合理,代码如下:

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class Combination4BigCar {  
  3.     private Car c;  
  4.     private int count;  
  5.     public Combination4BigCar(int batch, int count) {  
  6.         c = new Car(batch);  
  7.         this.count = count;  
  8.     }  
  9.     @Override  
  10.     public boolean equals(Object obj) {  
  11.         if (obj instanceof Combination4BigCar) {  
  12.             Combination4BigCar bc = (Combination4BigCar) obj;  
  13.             return c.equals(bc.c) && count == bc.count;  
  14.         }  
  15.         return false;  
  16.     }  
  17. }  

从代码来看即使batch相同,Combination4BigCar类的对象与Car类的对象间的比较也永远都是false,但是这样看起来也就合情合理了,毕竟Combination4BigCar也不是Car的子类,因此equals方法也就没必要提供任何对Car的比较支持,同时也不会违背了equals方法的传递性。

4.equals()的重写规则之必要性深入解读

前面我们一再强调了equals方法重写必须遵守的规则,接下来我们就是分析一个反面的例子,看看不遵守这些规则到底会造成什么样的后果。

[java] view plaincopy
  1. package com.zejian.test;  
  2. import java.util.ArrayList;  
  3. import java.util.List;  
  4. /** * 反面例子 * @author zejian */  
  5. public class AbnormalResult {  
  6.     public static void main(String[] args) {  
  7.         List<A> list = new ArrayList<A>();  
  8.         A a = new A();  
  9.         B b = new B();  
  10.         list.add(a);  
  11.         System.out.println("list.contains(a)->" + list.contains(a));  
  12.         System.out.println("list.contains(b)->" + list.contains(b));  
  13.         list.clear();  
  14.         list.add(b);  
  15.         System.out.println("list.contains(a)->" + list.contains(a));  
  16.         System.out.println("list.contains(b)->" + list.contains(b));  
  17.     }  
  18.     static class A {  
  19.         @Override  
  20.         public boolean equals(Object obj) {  
  21.             return obj instanceof A;  
  22.         }  
  23.     }  
  24.     static class B extends A {  
  25.         @Override  
  26.         public boolean equals(Object obj) {  
  27.             return obj instanceof B;  
  28.         }  
  29.     }  
  30. }  

上面的代码,我们声明了 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方法内部实现

[java] view plaincopy
  1. @Override         
  2. public boolean contains(Object o) {   
  3.      return indexOf(o) != -1;   
  4.  }  
进入indexof方法
[java] view plaincopy
  1.       @Override  
  2. ublic int indexOf(Object o) {  
  3. E[] a = this.a;  
  4. if (o == null) {  
  5.     for (int i = 0; i < a.length; i++)  
  6.         if (a[i] == null)  
  7.             return i;  
  8. else {  
  9.     for (int i = 0; i < a.length; i++)  
  10.         if (o.equals(a[i]))  
  11.             return i;  
  12. }  
  13. return -1;  

可以看出最终调用的是对象的equals方法,所以当调用20行代码list.contains(b)时,实际上调用了

b.equals(a[i]),a[i]是集合中的元素集合中的类型而且为A类型(只添加了a对象),虽然B继承了A,但此时

[java] view plaincopy
  1. a[i] instanceof B  
结果为false,equals方法也就会返回false;而当调用23行代码list.contains(a)时,实际上调用了a.equal(a[i]),其中a[i]是集合中的元素而且为B类型(只添加了b对象),由于B类型肯定是A类型(B继承了A),所以
[java] view plaincopy
  1. a[i] instanceof A  
结果为true,equals方法也就会返回true,这就是整个过程。但很明显结果是有问题的,因为我们的 list的泛型是A,而B又继承了A,此时无论加入了a还是b,都属于同种类型,所以无论是contains(a),还是contains(b)都应该返回true才算正常。而最终却出现上面的结果,这就是因为重写equals方法时没遵守对称性原则导致的结果,如果没遵守传递性也同样会造成上述的结果。当然这里的解决方法也比较简单,我们只要将B类的equals方法修改一下就可以了。
[java] view plaincopy
  1. static class B extends A{  
  2.         @Override  
  3.         public boolean equals(Object obj) {  
  4.             if(obj instanceof B){  
  5.                 return true;  
  6.             }  
  7.             return super.equals(obj);  
  8.         }  
  9.     }  

到此,我们也应该明白了重写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()计算一组散列码:

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class HashCodeTest {  
  3.     public static void main(String[] args) {  
  4.         int hash=0;  
  5.         String s="ok";  
  6.         StringBuilder sb =new StringBuilder(s);  
  7.           
  8.         System.out.println(s.hashCode()+"  "+sb.hashCode());  
  9.           
  10.         String t = new String("ok");  
  11.         StringBuilder tb =new StringBuilder(s);  
  12.         System.out.println(t.hashCode()+"  "+tb.hashCode());  
  13.     }  
  14.       
  15. }  

运行结果:

3548  1829164700

3548  2018699554

我们可以看出,字符串s与t拥有相同的散列码,这是因为字符串的散列码是由内容导出的。而字符串缓冲sb与tb却有着不同的散列码,这是因为StringBuilder没有重写hashCode方法,它的散列码是由Object类默认的hashCode方法计算出来的对象存储地址,所以散列码自然也就不同了。那么我们该如何重写出一个较好的hashCode方法呢,其实并不难,我们只要合理地组织对象的散列码,就能够让不同的对象产生比较均匀的散列码。例如下面的例子:

[java] view plaincopy
  1. package com.zejian.test;  
  2. public class Model {  
  3.     private String name;  
  4.     private double salary;  
  5.     private int sex;  
  6.       
  7.     @Override  
  8.     public int hashCode() {  
  9.         return name.hashCode()+new Double(salary).hashCode()   
  10.                 + new Integer(sex).hashCode();  
  11.     }  
  12. }  
上面的代码我们通过合理的利用各个属性对象的散列码进行组合,最终便能产生一个相对比较好的或者说更加均匀的散列码,当然上面仅仅是个参考例子而已,我们也可以通过其他方式去实现,只要能使散列码更加均匀(所谓的均匀就是每个对象产生的散列码最好都不冲突)就行了。不过这里有点要注意的就是java 7中对hashCode方法做了两个改进,首先java发布者希望我们使用更加安全的调用方式来返回散列码,也就是使用null安全的方法 Objects.hashCode(注意不是Object而是java.util.Objects) 方法,这个方法的优点是如果参数为null,就只返回0,否则返回对象参数调用的hashCode的结果。 Objects.hashCode 源码如下:
[java] view plaincopy
  1. public static int hashCode(Object o) {  
  2.         return o != null ? o.hashCode() : 0;  
  3.     }  
因此我们修改后的代码如下:
[java] view plaincopy
  1. package com.zejian.test;  
  2. import java.util.Objects;  
  3. public  class Model {  
  4.     private   String name;  
  5.     private double salary;  
  6.     private int sex;  
  7.     @Override  
  8.     public int hashCode() {  
  9.         return Objects.hashCode(name)+new Double(salary).hashCode()   
  10.                 + new Integer(sex).hashCode();  
  11.     }  
  12. }  
java 7还提供了另外一个方法java.util.Objects.hash(Object... objects),当我们需要组合多个散列值时可以调用该方法。进一步简化上述的代码:
[java] view plaincopy
  1. package com.zejian.test;  
  2. import java.util.Objects;  
  3. public  class Model {  
  4.     private   String name;  
  5.     private double salary;  
  6.     private int sex;  
  7. //  @Override  
  8. //  public int hashCode() {  
  9. //      return Objects.hashCode(name)+new Double(salary).hashCode()   
  10. //              + new Integer(sex).hashCode();  
  11. //  }  
  12.       
  13.     @Override  
  14.     public int hashCode() {  
  15.         return Objects.hash(name,salary,sex);  
  16.     }  
  17. }  

好了,到此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深入解析一书)

[java] view plaincopy
  1. package com.zejian.test;  
  2. import java.util.HashMap;  
  3. import java.util.Map;  
  4. public class MapTest {  
  5.     public static void main(String[] args) {  
  6.         Map<String,Value> map1 = new HashMap<String,Value>();  
  7.         String s1 = new String("key");  
  8.         String s2 = new String("key");    
  9.         Value value = new Value(2);  
  10.         map1.put(s1, value);  
  11.         System.out.println("s1.equals(s2):"+s1.equals(s2));  
  12.         System.out.println("map1.get(s1):"+map1.get(s1));  
  13.         System.out.println("map1.get(s2):"+map1.get(s2));  
  14.           
  15.           
  16.         Map<Key,Value> map2 = new HashMap<Key,Value>();  
  17.         Key k1 = new Key("A");  
  18.         Key k2 = new Key("A");  
  19.         map2.put(k1, value);  
  20.         System.out.println("k1.equals(k2):"+s1.equals(s2));  
  21.         System.out.println("map2.get(k1):"+map2.get(k1));  
  22.         System.out.println("map2.get(k2):"+map2.get(k2));  
  23.     }  
  24.       
  25.     /** 
  26.      * 键 
  27.      * @author zejian 
  28.      * 
  29.      */  
  30.     static class Key{  
  31.         private String k;  
  32.         public Key(String key){  
  33.             this.k=key;  
  34.         }  
  35.           
  36.         @Override  
  37.         public boolean equals(Object obj) {  
  38.             if(obj instanceof Key){  
  39.                 Key key=(Key)obj;  
  40.                 return k.equals(key.k);  
  41.             }  
  42.             return false;  
  43.         }  
  44.     }  
  45.       
  46.     /** 
  47.      * 值 
  48.      * @author zejian 
  49.      * 
  50.      */  
  51.     static class Value{  
  52.         private int v;  
  53.           
  54.         public Value(int v){  
  55.             this.v=v;  
  56.         }  
  57.           
  58.         @Override  
  59.         public String toString() {  
  60.             return "类Value的值-->"+v;  
  61.         }  
  62.     }  
  63. }  
代码比较简单,我们就不过多解释了(注意Key类并没有重写hashCode方法),直接运行看结果
[java] view plaincopy
  1. s1.equals(s2):true  
  2. map1.get(s1):类Value的值-->2  
  3. map1.get(s2):类Value的值-->2  
  4. k1.equals(k2):true  
  5. map2.get(k1):类Value的值-->2  
  6. map2.get(k2):null  
对于s1和s2的结果,我们并不惊讶,因为相同的内容的s1和s2获取相同内的value这个很正常,因为String类重写了equals方法和hashCode方法,使其比较的是内容和获取的是内容的哈希码。但是对于k1和k2的结果就不太尽人意了,k1获取到的值是2, k2获取到的是null,这是为什么呢?想必大家已经发现了,Key只重写了equals方法并没有重写hashCode方法,这样的话,equals比较的确实是内容,而hashCode方法呢?没重写,那就肯定调用超类Object的hashCode方法,这样返回的不就是地址了吗?k1与k2属于两个不同的对象,返回的地址肯定不一样,所以现在我们知道调用map2.get(k2)为什么返回null了吧?那么该如何修改呢?很简单,我们要做也重写一下hashCode方法即可(如果参与equals方法比较的成员变量是引用类型的,则可以递归调用hashCode方法来实现):
[java] view plaincopy
  1. @Override  
  2. public int hashCode() {  
  3.      return k.hashCode();  
  4. }  
再次运行:
[java] view plaincopy
  1. s1.equals(s2):true  
  2. map1.get(s1):类Value的值-->2  
  3. map1.get(s2):类Value的值-->2  
  4. k1.equals(k2):true  
  5. map2.get(k1):类Value的值-->2  
  6. map2.get(k2):类Value的值-->2  

6.重写equals()中getClass与instanceof的区别

虽然前面我们都在使用instanceof(当然前面我们是根据需求(批次相同即相等)而使用instanceof的),但是在重写equals() 方法时,一般都是推荐使用 getClass 来进行类型判断(除非所有的子类有统一的语义才使用instanceof),不是使用 instanceof。我们都知道 instanceof 的作用是判断其左边对象是否为其右边类的实例,返回 boolean 类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。下来我们来看一个例子:父类Person

[java] view plaincopy
  1. public class Person {  
  2.         protected String name;  
  3.         public String getName() {  
  4.             return name;  
  5.         }  
  6.         public void setName(String name) {  
  7.             this.name = name;  
  8.         }  
  9.         public Person(String name){  
  10.             this.name = name;  
  11.         }  
  12.         public boolean equals(Object object){  
  13.             if(object instanceof Person){  
  14.                 Person p = (Person) object;  
  15.                 if(p.getName() == null || name == null){  
  16.                     return false;  
  17.                 }  
  18.                 else{  
  19.                     return name.equalsIgnoreCase(p.getName ());  
  20.                 }  
  21.             }  
  22.             return false;  
  23.        }  
  24.     }  
子类 Employee
[java] view plaincopy
  1. public class Employee extends Person{  
  2.         private int id;  
  3.         public int getId() {  
  4.             return id;  
  5.         }  
  6.         public void setId(int id) {  
  7.             this.id = id;  
  8.         }  
  9.         public Employee(String name,int id){  
  10.             super(name);  
  11.             this.id = id;  
  12.         }  
  13.         /** 
  14.          * 重写equals()方法 
  15.          */  
  16.         public boolean equals(Object object){  
  17.             if(object instanceof Employee){  
  18.                 Employee e = (Employee) object;  
  19.                 return super.equals(object) && e.getId() == id;  
  20.             }  
  21.             return false;  
  22.         }  
  23.     }  
上面父类 Person 和子类 Employee 都重写了 equals(),不过 Employee 比父类多了一个id属性,而且这里我们并没有统一语义。测试代码如下:
[java] view plaincopy
  1. public class Test {  
  2.         public static void main(String[] args) {  
  3.             Employee e1 = new Employee("chenssy"23);  
  4.             Employee e2 = new Employee("chenssy"24);  
  5.             Person p1 = new Person("chenssy");  
  6.             System.out.println(p1.equals(e1));  
  7.             System.out.println(p1.equals(e2));  
  8.             System.out.println(e1.equals(e2));  
  9.         }  
  10.     }  
上面代码我们定义了两个员工和一个普通人,虽然他们同名,但是他们肯定不是同一人,所以按理来说结果应该全部是 false,但是事与愿违,结果是:true、true、false。对于那 e1!=e2 我们非常容易理解,因为他们不仅需要比较 name,还需要比较 ID。但是 p1 即等于 e1 也等于 e2,这是非常奇怪的,因为 e1、e2 明明是两个不同的类,但为什么会出现这个情况?首先 p1.equals(e1),是调用 p1 的 equals 方法,该方法使用 instanceof 关键字来检查 e1 是否为 Person 类,这里我们再看看 instanceof:判断其左边对象是否为其右边类的实例,也可以用来判断继承中的子类的实例是否为父类的实现。他们两者存在继承关系,肯定会返回 true 了,而两者 name 又相同,所以结果肯定是 true。所以出现上面的情况就是使用了关键字 instanceof,这是非常容易导致我们“钻牛角尖”。故在覆写 equals 时推荐使用 getClass 进行类型判断。而不是使用 instanceof(除非子类拥有统一的语义)。

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 方法的常规协定,该协定声明 相等对象必须具有相等的哈希码 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558946.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java8————Optional

引言 Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true&#xff0c;调用get()方法会返回该对象。 Optional 是个容器&#xff1a;它可以保存类型T的值&#xff0c;或者仅仅保存null。Optional提供很多有用的方法&#xff0c;这样我们就不用显式进…

问题反馈信息处理平台开发过程

问题反馈信息处理平台开发过程 “问题反馈信息处理平台”是一个将用户反馈上来的出错信息进行处理和收集的一个平台。 这个项目主要都是在实习的时候由我一个人进行开发&#xff0c;我导师在旁边进行指导完成的。 该项目的技术架构主要是&#xff1a; 前端主要基于Vue框架的…

Java8————Base64

Base64&#xff1f; Base64是一种用64个字符来表示任意二进制数据的方式。 对于二进制文件如图片、exe、音频、视频等&#xff0c;包含很多无法显示和打印的字符&#xff0c;如果希望能够通过记事本这样的文本处理软件处理二进制数据&#xff0c;就需要一个二进制转字符串的转…

大众点评后端项目解析

restful Api是一种设计风格&#xff1a; 启动前端项目&#xff1a;在前端项目根目录 npm install&#xff1a;加载依赖包 npm run mock&#xff1a;提供模拟数据的接口&#xff0c;前端脱离于后台&#xff1b;start /b npm run mock&#xff08;于后台运行&#xff09; npm…

Java中类及方法的加载顺序

代码展示 请运行下面代码&#xff0c;查看运行结果&#xff0c;并带着问题&#xff0c;尝试第二次debug程序。 class A {private static int numA;private int numA2;static {System.out.println("A的静态字段 : " numA);System.out.println("A的静态代码块…

新手入门教程-------Spring Boot中集成RabbitMQ

AMQP&#xff1a;是Advanced Message Queuing Protocol的简称&#xff0c;高级消息队列协议&#xff0c;是一个面向消息中间件的开放式标准应用层协议。 定义了以下特性&#xff1a; 消息方向消息队列消息路由&#xff08;包括&#xff1a;点到点和发布-订阅模式&#xff09;可…

Java 多线程 —— ReentrantLock 与 Condition

引言 ReentrantLock 是 JUC 下的一个功能强劲的锁工具&#xff0c;支持公平锁、非公平锁&#xff0c;以及多等待队列的 Condition 。 也常常被称为“手动锁”。本篇博客主要分析它的使用方法以及 Condition 实现的一个生产者消费者模式。 一、可替代 synchronized 的手动锁 …

Rabbitmq+Springboot设计秒杀应用

秒杀业务的核心是库存处理&#xff0c;用户购买成功后会进行减库存操作&#xff0c;并记录购买明细。当秒杀开始时&#xff0c;大量用户同时发起请求&#xff0c;这是一个并行操作&#xff0c;多条更新库存数量的SQL语句会同时竞争秒杀商品所处数据库表里的那行数据&#xff0c…

Java8————Stream API

引言 Java8 加入了java.util.stream包&#xff0c;这个包中的相关API将极大的增强容器对象对元素的操作能力。 它专注于对集合对象进行各种便利、高效的聚合操作&#xff0c;或大批量数据处理。 Stream API借助于同样新出现的Lambda表达式&#xff0c;极大的提高了编程效率和…

MySQL数据库知识点总结

数据库&#xff1a; 数据库索引的好处&#xff1a;索引是对数据库表中的一个或多个列的值进行排序的结构&#xff0c;这样检索或者查询某条记录的时候就不在是顺序查找&#xff0c;而是使用特定的查找方式进行查找&#xff0c;如通过二分查找或者是hash值来查找&#xff0c;提高…

Java8 函数式对齐约定————Eclipse自定义代码风格

引言 Java8 的函数式代码风格在Stream的使用上尤为突出。尽管我们可以通过连续调用函数完成一系列操作&#xff0c;但是其可读性并不能保证&#xff0c;还需要有与之相辅的Code Style。例如&#xff0c;请尝试阅读下面两段完全相同的代码&#xff1a; 未遵守约定格式&#xf…

Java核心篇之JVM--day3

Java核心篇之JVM--day3 Java JVM详解--通俗易懂教程 JVM&#xff1a;Java虚拟机的简称。 谈到JVM&#xff0c;通常会聊到三个问题&#xff1a; 1. 什么时候触发Java GC&#xff1f; 2. 对什么东西进行Java GC&#xff1f; 3. 如何进行Java GC&#xff1f; 首先解决第…

使用springboot来实现WebLog

使用websocket技术实时输出系统日志到浏览器端&#xff0c;实现WebLog boot-websocket-log&#xff1a; spring boot系统中使用websocket技术实时输出系统日志到浏览器端&#xff0c;因为是实时输出&#xff0c;所有第一时间就想到了使用webSocket,而且在spring boot中&#…

设计模式---观察者模式介绍与理解

设计模式---观察者模式介绍与理解&#xff1a; 观察者模式原理&#xff1a;类似于定牛奶业务 1. 奶站&#xff0c;subject&#xff1a;登记注册&#xff0c;移除&#xff0c;通知&#xff08;register&#xff0c;remove&#xff0c;notify&#xff09; 2. 用户&#xff0c;…

CRS-4995: The command ‘start resource’ is invalid in crsctl.

ntp时间调整后&#xff0c;节点1&#xff0c;advm 和acfs offline 处理办法&#xff1a; /u01/app/12.2.0.1/grid/bin/crsctl stop crs /u01/app/12.2.0.1/grid/bin/crsctl start crs 曾经尝试如下命令不起作用 /u01/app/12.2.0.1/grid/bin/acfsload start /u01/app/12.2…

抽象工厂模式升级版————泛型化实现

引言 今天回看之前总结的抽象工厂模式的实现《Java常用设计模式————抽象工厂模式》&#xff0c;聚焦于抽象工厂模式的缺点&#xff0c;试着改进了一下。 回顾一下抽象工厂模式的缺点&#xff1a; 在添加新的产品类型时&#xff0c;难以扩展抽象工厂来生产新种类的产品。…

发生在“注解”@的那些事儿

注解&#xff1a; 自定义注解&#xff1a; 注解和类&#xff0c;接口一样&#xff0c;属于一种数据类型 注解可以放在类&#xff0c;方法&#xff0c;属性上面 注解可以有属性&#xff0c;也可以没有属性 注解有作用范围 &#xff08; 源码期间&#xff08;String&#…

Java常用设计模式————建造者模式

引言 建造者模式&#xff08;Builder Pattern&#xff09;使用多个简单对象一步一步构建成一个复杂的对象。这种类型的设计模式属于建造型模式&#xff0c;它提供了一种创建对象的最佳方式。 一个Builder会一步步构建最终的对象。该Builder类是独立于其他对象的。 实现概要 …

使用动态代理解决网站字符集编码问题:(之前通过拦截器)

使用动态代理解决网站字符集编码问题&#xff1a;&#xff08;之前通过拦截器&#xff09; 设计模式&#xff1a; 在软件开发的过程中&#xff0c;遇到相识的问题&#xff0c;将问题的解决方式抽象为模型&#xff08;套路&#xff09; 单例模式&#xff08;静态代码只会执行一…

设计模式---简单工厂设计模式

先定义一个抽象类Animal&#xff1a; 定义两个动物类继承这个类&#xff1a; 定义一个专门生产动物的工程类&#xff1a; 最后定义一个测试类&#xff1a; 按照这个动物工厂类&#xff0c;你会发现&#xff0c;如果动物一多的话&#xff0c;就需要写很多重复的方法&#xff0c;…