Java 类对象相等的实现——覆盖equals方法

1,376 阅读6分钟
  • 两个类对象相等是指什么相等?
  • Java语言规范要求的equals方法特性
  • 超类Object 提供的equals方法
  • 子类实现完美个性化equals(getClass VS instanceof)
----------------------------------------------------------------------------------------------

一、两个类对象相等是指什么相等?

由于多态的存在,一个父类对象变量可能指向子类对象,为判断两个对象相等增加了复杂度。
对象相等分为两种: 地址相等 和 属性值相等。
----------------------------------------------------------------------------------------------

二、Java语言规范要求的equals方法特性

自反性:对于任何非空引用x, x.equals(x) 结果返回ture
对称性:对于任何引用x和y,x.equals(y) == y.equals(x)
传递性:对于任何引用x、y和z, x.equals(y)== y.equals(z) == z.equals(x)
一致性:如果x和y引用的对象没有发生变化,x.equals(y)的返回结果应该不变
对于任意非空引用x, x.equals(null)返回false
-----------------------------------------------------------------------------------------------

三、超类Objects 提供的equals方法

如果没有对超类的equals方法重写的话,Object.equals相当于 ==
判断的是两个对象变量引用的对象地址是否相同(是否引用同一对象)
在Object.equals(obj1,obj2)中,
1.两个参数地址都为null ,返回true;
2.两个参数地址≠null 且 相等,返回true;
3.其中一个参数为null,返回false;
4.两个参数地址≠null 且 不等, 返回false。
-----------------------------------------------------------------------------------------------

四、子类实现完美个性化equals

子类重写equals方法,按照以下建议:
1.如黄色荧光
ChildObj.equals(Object otherObj)

2.检测this 和 otherObj 是否引用统一个对象:
if(this == otherObj ) return true;
这句用于优化,因为判断this 和 otherObj 地址相等与否容易得多,可以及早返回结果,避免执行下面的比较过程。

3.检测 otherObj 是否为null,如果为null,返回false
if( otherObj == null ) return false;
这句是十分必要的,因为otherObj如果为null,在后续代码中编译 otherObj.方法 或 otherObj.属性 ,编译器会报错。
个人认为,第2点和第3点的顺序没有一定的先后关系。

4.比较this 与 otherObj 是否属于同一类
①如果由父类决定相等概念,如:父类A有属性id和name///子类B继承父类A,增加自己的属性age。
判断A和B是否相等,只需判断他们共有的来自父类的id 和 name 属性是否一致。
这时候可用关键字 instanceof 作为筛选条件,以让程序进入比较id 和 name 的步骤(第6步)。
instanceof 实现方法可见:
if(!otherObj instanceof FatherClass ) return false;
plus:继承自同一父类的各子类之间也可以instanceof进入第6步,第5步的时候都转化为父类对象变量即可。

②如果严格限定必须是同一类才有相等的概念:使用 getClass()方法检测,以决定是否进入第6步。
if(getClass() != otherObj.getClass()) return false;

5.将otherObj 转换成相应的类类型变量(因为形参类型是Object)
① 如果第4步用了instanceof,有两种情况:
(1)父类对象变量 指引 父类对象
(2)父类对象变量 指引 子类对象(多态)
(3)父类对象变量 指引 其他子类对象(多态)
以下详解:
(1)父类对象变量 指引 父类对象 //注:比getClass()方法多出来的比较类型1
FatherClass A = new FatherClass();
ChildClass B = new ChildClass();
又分两种情况:
[1]父类.equals(子类) //调用的是父类的equals
[2]子类.equals(父类) //调用的是子类的equals
上述两种情况须满足自反性,所以父类和子类定义的equals必须一致才能使用instanceof!
在第5步里它们都是将传入的对象转为父类类型,区别在第6步。

FatherClass other = (FatherClass) otherObj;
(2)父类对象变量 指引 子类对象(多态)
FatherClass A = new ChildClass();
ChildClass B = new ChildClass();
又分两种情况:
[1]父类对象变量.equals(子类) //调用的是父类的equals
[2]子类对象变量.equals(父类) //调用的是子类的equals
自反性:父子定义的equals仍然必须一致才能使用instanceof.
用instanceof的时候,为了不判断是 同一类之间比较 还是 父子类之间 的比较
类型干脆都转为父类(何况父类不能转为子类),所以还是这一句:
FatherClass other = (FatherClass) otherObj;

(3)父类对象变量 指引 其他子类对象(多态)//注:比getClass()方法多出来的比较类型2
也是统统转化为父类类型,并且父类和各子类的equals实现必须一致,才能确保自反性。

② 如果第4步用了getClass()方法,除了只能比较同一类外(允许多态),自反性上诸事大吉:
因为同一类,把参数传进的变量对象类型转换成equals调用方类型(就是这个类)即可
thisClass other = (thisClass) otherObj;
如果两个子类对象都是由同一类的子类对象变量指引的,任意一个做调用方都是没问题的。
如果是多态的情况:这种getClass()方法会返回对象变量指向的对象的实际类型。所以如果父类对象变量
指向的子类对象和要比较的另一子类对象不是一类,第4步就会返回false.


6.比较所有需要比较的属性(域)
用 == 比较 基本类型域 ,用equals比较 对象域 。当且仅当全部域都相等才返回true。
问题:Java类内方法是类共有的(C++中类内方法是所有类共享一份,Java是否也是?),类方法可以访问该类
所有对象的私有属性。因此:
①第4步用了instanceof,若被比较对象不是同一类:
class FatherClass{
     private String name = "Father";
     private int id = 1;
    
     public boolean equals(Object otherObj){
         
         if(this == otherObj) return true;
         
         if(otherObj == null) return false;
         
         if(! (otherObj instanceof FatherClass)) return false;
         
         FatherClass other = (FatherClass) otherObj;
         
         return name.equals(other.name) && (id == other.id);  //main方法中那样调用,直接访问对象私有属性没问题。
     }
}
class ChildClass1 extends FatherClass{
     private String name = "Child1";
     private int id = 1;
     private int age = 2;
    /**从 父类 继承得到equals*/
}
class ChildClass2 extends FatherClass{
     private String name = "Child1";
     private int id = 1;
     private int age = 3;
    /**从 父类 继承得到equals*/
}
public class MainTest{
     public static void main(String[] args){
            FatherClass A = new FatherClass();
            ChildClass1 B = new ChildClass1();
            ChildClass2 C = new ChildClass2();
                
            A.equals(B);    //父类方法可以访问子类继承到的私有属性
            B.equals(A);    //子类继承得到的方法可以访问父类的所有私有属性    
            B.equals(C);    //兄弟间也是允许用父类中的方法访问彼此从父类继承得到的私有属性的
     }
}
②getClass() 类可以访问该类所有对象的私有属性,所以都ok。