如何重写Java中的equals、hascode方法

 Java中,只有8种基本类型不是对象,例如:4种整形类型(byte, short, int,long),2种浮点类型(flout, double),boolean, char不是对象,其他的所有类型,不论是对象数组,列表等都扩展了Object类.了解学习Object中方法的设计原理和实现方式有助于更好的学习理解java语言.

下面,我们首先学习一下Object中的equals方法.判断两个对象相等时,JVM首先查找两个对象的hashCode, 如果两者hashCode不同,则返回false;如果两者的hashCode相同,则调用equals方法进行比较.

Object中equals方法 

   Object中的equals方法用于检测一个对象是否等于另外一个对象. Object类中,判断两个对象是否引用同一个对象实现equals方法,源码如下:

    public  boolean equals(Object obj) {
         return ( this == obj);
    }

    实际处理中,若要比较两个对象是否相等,一般需要重写equals方法.

重写equals方法的要点: 
1. 使用==操作符检查“实参是否为指向对象的一个引用”。

2.判断实参是否为null
3. 使用instanceof操作符检查“实参是否为正确的类型”。 
4. 把实参转换到正确的类型。 
5. 对于该类中每一个“关键”域,检查实参中的域与当前对象中对应的域值是否匹 
  配。对于既不是float也不是double类型的基本类型的域,可以使用==操作符 
  进行比较;对于对象引用类型的域,可以递归地调用所引用的对象的equals方法; 
  对于float类型的域,先使用Float.floatToIntBits转换成int类型的值, 
  然后使用==操作符比较int类型的值;对于double类型的域,先使用 
  Double.doubleToLongBits转换成long类型的值,然后使用==操作符比较 
  long类型的值。 
6. 当你编写完成了equals方法之后,应该问自己三个问题:它是否是对称的、传 
  递的、一致的?(其他两个特性通常会自行满足)如果答案是否定的,那么请找到 
  这些特性未能满足的原因,再修改equals方法的代码。


例1:雇员对象比较

如果两个雇员对象的姓名, 薪水和雇佣一样,就认为它们相等.重写equals方法如下:

复制代码
public  class Employee {

     private String name;
     private  double salary;
     private Date hireDay;

    ... 

    @Override
    public boolean equals(Object obj) {
        // 如果为同一对象的不同引用,则相同
        if (this == obj) {
            return true;
        }
        // 如果传入的对象为空,则返回false
        if (obj == null) {
            return false;
        }

        // 如果两者属于不同的类型,不能相等
        if (getClass() != obj.getClass()) {
            return false;
        }

        // 类型相同, 比较内容是否相同
        Employee other = (Employee) obj;
        return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
    }

复制代码

  注意,比较通过Objects中静态函数equals比较两个对象是否相等.该方法源码如下:

  public  static  boolean equals(Object a, Object b) {
         return (a == b) || (a !=  null && a.equals(b));
    }

 这样,当两个对象都为null时,返回true,例如,两个Employee对象的name都为null, 返回true; 如果第一个对象不为null,则调用a.equals(Object obj)方法

 

对象比较与继承

   如果隐式和显式的对象所属类型不一致,equals方法该如何处理呢?在例1中,如果两个对象的类型不一致,直接返回false;多数程序员直接使用instanceof检测:

if(!(obj  instanceof Employee))  return  false;

 这样做不但没有解决继承子类的错误,还可能引发其他的一些错误,例1中,采用强制类型转换的方式.

 要实现子类对象比较,先了解一下java规范中对equals方法的约束.

java规范中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)应该返回同样的结果.

  • 对任意非空引用x, x.equals(null) 返回false;

 下面可以通过两个不同的情况看待这个问题:

  • 如果子类能够拥有自己的相等概念, 则对称性需求强制采用getClass进行检测
  • 如果由超类决定相等的概念, 那么就用instanceof进行检测,这样可以在不用子类的对象之间进行相等的比较
 注释:
  1. 在标准的java库中包含150多个equals方法的实现,包括instanceof检测, 调用getClass检测, 捕获ClassCastException检测或者什么都不做. 在java.sql.TimeStamp实现人员指出, Timestamp类继承Date类,而后者的equals方法使用了一个instanceof检测,这样重写equals方法时,就无法同时做到对称性.
  2. 在由超类决定相等时,可以考虑final关键字修改比较函数,若考虑到子类equals方法灵活性,可以不加修饰,例如AbstractSet.equals方法,应该申明为final, 这样就可以比较子类HashSet和TreeSet, 但是考虑到子类的灵活性,没有添加任何修饰.

编写equals方法的建议:

  1. 显示参数命名为otherObject, 稍后转化成other变量
    public  boolean equals(Object otherObject)
  2. 检测this和otherObject是否是同一个对象的引用,是,返回true;
    if( this==otherObject){
         return  true;
    }
  3. 检测otherObject是否为null, 是, 返回false;
    if(otherObject ==  null){
         return  false;
    }
  4. 比较this和otherObject是否属于同一个类. 如果equals的语义在每个子类中有所改变,就使用getClass检测:
    if(getClass() != otherObject.getClass()){
         return  false;
    }
    如果所以子类语义相同,使用instanceof检测:
    if(!(otherObject  instanceof Employee)){
         return  false;
    }
  5. 将otherObject转化为相对应的类型变量other
    Employee other = (Employee)otherObject;
  6. 对所需要的比较的数据域进行比较. 如果是基本数据类型,使用a==b比较; 如果是对象比较,调用Objects.equals(a, b)进行比较
    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);

整个流程可以参照例1;

常见equals方法实现错误

复制代码
public  class Employee {
     public  boolean equals(Employee other) {


     return Objects.equals(name, other.name) && salary ==other.salary && Objects.equals(hireDay, other.hireDay);
}
复制代码

 

这个方法声明的显示参数类型是Employee. 其结果并没有覆盖Object中的equals方法,而是定义了一个完全无关的方法.为了避免发生类型错误,可以使用@override对覆盖超类的方法进行标记.


  在Java中,问什么说重写了equals方法都要进而重写Hashcode方法呢?

首先,想要明白hashCode的作用,你必须要先知道Java中的集合。  
总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set
你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。
那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?
这就是Object.equals方法了
。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。
也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000equals方法。这显然会大大降低效率。    
于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。
哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。
初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。   
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。
如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,
就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。
所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。 

原因如下:当equals此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。如下:
(1)当obj1.equals(obj2)为true时,obj1.hashCode() == obj2.hashCode()必须为true 
(2)当obj1.hashCode() == obj2.hashCode()为false时,obj1.equals(obj2)必须为false

hashcode是用于散列数据的快速存取,如利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的。

这样如果我们对一个对象重写了euqals,意思是只要对象的成员变量值都相等那么euqals就等于true,但不重写hashcode,那么我们再new一个新的对象,当原对象.equals(新对象)等于true时,两者的hashcode却是不一样的,由此将产生了理解的不一致。

——————————————————————————————————————————

正确重写hashCode的办法

[1]. hashCode重写成相同的值缺点

将所有对象的hashCode都返回一样的值是不科学的。比如a1和a3这两个根本不同的对象,就没有必要去比较equals,增加无谓的计算量所以应该对象本身的内容 (属性)来重写hashCode。

一旦两个对象内部不一样,就直接判定出hashCode不一样不用再调用equals进行比较

[2]. 正确书写hashCode的办法:

【原则】按照equals( )比较两个对象是否一致条件用到的属性重写hashCode()

{1}. 常用的办法就是利用涉及到的的属性进行线性组合

{2}. 线性组合过程中涉及到的组合系数自定义即可。

注意,拼接之后的数值不能超过整形的表达范围。

{3}. 公式:属性1int形式+ C1*属性2int形式+  C2*属性3int形式+ …

【技巧】属性是引用类型的时候,如果已经重写过hashCode(),那么这个引用属性的int形式就是直接调用属性已有的hashCode值。

最典型的就是这个属性是字符串类型的,String类型已经重写了hashCode()方法,所以直接拿来使用即可。

——————————————————————————————————————————

要重写自己的hashCode方法并没有什么绝对正确的答案,但是我们的目标是:不相等的对象尽可能有不同的hashCode,而且必须满足的一个通用约定是:相等的对象应该具有相同的hashCode。下面介绍一种hashCode的实现方式,这种实现方式对一般的程序来说足够了,至于如何实现更完美的hashCode方法就留给数学家或者理论家去讨论吧。

第一步:定义一个初始值,一般来说取17

int result = 17;

第二步:分别解析自定义类中与equals方法相关的字段(假如hashCode中考虑的字段在equals方法中没有考虑,则两个equals的对象就很可能具有不同的hashCode)

    情况一:字段a类型为boolean 则[hashCode] = a ? 1 : 0;

    情况二:字段b类型为byte/short/int/char, 则[hashCode] = (int)b;

    情况三:字段c类型为long, 则[hashCode] = (int) (c ^ c>>>32);

    情况四:字段d类型为float, 则[hashCode] = d.hashCode()(内部调用的是Float.hashCode(d), 而该静态方法内部调用的另一个静态方法是Float.floatToIntBits(d))

    情况五:字段e类型为double, 则[hashCode] = e.hashCode()(内部调用的是Double.hashCode(e), 而该静态方法内部调用的另一个静态方法是Double.doubleToLongBits(e),得到一个long类型的值之后,跟情况三进行类似的操作,得到一个int类型的值)

    情况六:引用类型,若为null则hashCode为0,否则递归调用该引用类型的hashCode方法。

    情况七:数组类型。(要获取数组类型的hashCode,可采用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + ..... + s[n-1], 该方法正是String类的hashCode实现所采用的算法

第三步:对于涉及到的各个字段,采用第二步中的方式,将其依次应用于下式:

result = result * 31 + [hashCode];

补充说明一点:如果初始值result不取17而取0的话,则对于hashCode为0的字段来说就没有区分度了,这样更容易产生冲突。比如两个自定义类中,一个类比另一个类多出来一个或者几个字段,其余字段全部一样,分别new出来2个对象,这2个对象共有的字段的值全是一样的,而对于多来的那些字段的值正好都是0,并且在计算hashCode时这些多出来的字段又是最先计算的,这样的话,则这两个对象的hashCode就会产生冲突。还是那句话,hashCode方法的实现没有最好,只有更好。

  1.  public int hashCode() {  
  2.          int hash = 7;  
  3.          hash = 31 * hash + num;  
  4.          hash = 31 * hash + (null == data ? 0 : data.hashCode());  
  5.          return hash;  
  6.   }  


先调用hashCode,若相同,再调用equals!!!
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值