Java中Object比较方法equals使用和重写深度刨析

  • Post author:
  • Post category:java




getClass()

getClass()方法用来获取调用该方法对象的引用类型

class Test{
    int a;
    Test(int a){
        this.a = a;
    }
}

public class Main {
    public static void main(String[] args) {
        Test a = new Test(3);
        Object b = new Test(5);
        System.out.println(a.getClass());//输出class Test
        System.out.println(b.getClass());//输出class Test
    }
}



equals()

equals方法在Object类中是用==实现的,保证了equals的通用性。但是由于等号在比较引用类型时比较的是两个对象的地址,因此我们往往需要重写该方法。

Object对该方法的实现:

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

对于上面的Test类型,我们想通过比较两个对象的a成员比较,我们可以用下面的代码进行重写:

@Override
    public boolean equals(Object o) {
        Test tmp = (Test) o;
        return a == tmp.a;
    }

@Override用来告诉编译器下面这个方法是重写的父类方法,如果超类不存在该方法(超类中没有名称和参数列表与之相同的方法)编译器就会报错。

此时我们调用以下代码:

public class Main {
    public static void main(String[] args) {
        Test a = new Test(3);
        Object b = new Test(5);
        Object c = new Test(3);
        System.out.println(a.equals(b));//false
        System.out.println(a.equals(c));//true
    }
}

结果似乎已经满足了我们的要求,但是这个方法仍然具有很大的问题,如果equals中的参数无法强转为Test类型,那运行时程序会抛出异常,如下:

public class Main {
    public static void main(String[] args) {
        Test a = new Test(3);
        A b = new A();//A是一个与Test无关的类型
        System.out.println(a.equals(b));
    }
}

那么我们该如何解决这种情况呢,很简单,如果它们不是一个类型就返回false就行了,代码如下:

public boolean equals(Object o) {
        if(this.getClass() != o.getClass()){//注意前面所说,getClass是调用者的引用类型,这里o.getClass返回的不一定是Object
            return false;
        }
        Test tmp = (Test) o;
        return a == tmp.a;
    }

有些人也会使用instanceof判断参数能否转化为需要转化的类型,一般情况下它们两者并无区别,代码如下:

@Override
    public boolean equals(Object o){
        if(o instanceof Test == false){
            return false;
        }
        Test tmp = (Test) o;
        return a == tmp.a;
    }


补充:

instanceof用来测试左边的对象是否是

右边类

或者

该类的子类

创建的实例对象

那么它们是不是都能随便换着用呢?并不是,接下来我会用《Java核心技术 卷一》的说明和例子介绍使用它们的区别。

Java语言规范要求equals方法具有下面的特性:

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

2)对称性:对于任何引用x和y,当且仅当y.equals(x)返回true, x.equals(y)也应该返回true。

3)传递性:对于任何引用x、y和z,如果x.equals(y)返回true, y.equals(z)返回true, x.equals(z)也应该返回true。

4)一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。

5)对于任意非空引用x, x.equals(null)应该返回false。

由于使用instanceof比较时可以比较该类的子类,所以使用instanceof的equals方法通常是在某个祖先类的所有派生类都是通过某些属性是否相等判定的,这些属性往往具有唯一性,例如每一个人的身份证号,例如下面的两个类:

class Employee{
    int id;
    public String name;

    public Employee(int id,String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o){
        if(o instanceof Employee == false){
            return false;
        }
        Employee tmp = (Employee) o;
        return this.id == tmp.id;
    }
}

class Manager extends Employee{
    public double bonus;

    public Manager(int id,String name, double bonus) {
        super(id,name);
        this.bonus = bonus;
    }
}

public class Main {
    public static void main(String[] args) {
        Employee employee = new Employee(1,"jack");
        Manager manager = new Manager(1,"jack",500);
        System.out.println(employee.equals(manager));//true
        System.out.println(manager.equals(employee));//true
    }
}

由于每个职员的id都是唯一的,我们只要用id就可以比较所有员工,如果我们这时用getClass来写equals会怎么样呢?结果是两个输出都是false,因为employee和manager的引用类型是不同的,所以两个getClass返回的值也不同,也就是说使用getClass时Manager只能和Manager比较,Empoyee只能和Employee比较。


注意:

由于使用 instanceof的equals是通用性的,所以我们一般会把使用instanceof的equals设置为final方法来防止子类重写该方法。

getClass也有它自己的用途,因为instanceof的局限性,如果用instanceof就无法让两个Manager类型的对象比较它们Manager特有的bouns了。换种说法,如果我们想比较的是子类特有的属性,就需要在每个子类中用getClass重写equals。 由于奖金这个东西是经理特有的,我们想要使用Manager的equals来比较bonus就必须在Manager重写用getClass实现的equals。

在这里我引用一下《Java核心技术 卷一》里说明的规则

● 如果子类能够拥有自己的相等概念,则对称性需求将强制采用getClass进行检测。

● 如果由超类决定相等的概念,那么就可以使用instanceof进行检测,这样可以在不同子类的对象之间进行相等的比较。

或许讲到这里你要放松一口气了,但很可惜,这个方法仍旧有一些问题,但放心,只是一个小问题。但问题虽小,如果你忘了的话很可能会酿成大祸!请看如下代码:

public class Main {
    public static void main(String[] args) {
        Employee employee1 = new Employee(1,"jack");
        Employee employee2 = null;
        System.out.println(employee1.equals(employee2));
    }
}

是的,你比较了一个空引用!这结果理应返回false,但是编译器可不知道,解决的方法也很简单,如果o为null返回false就好了!

@Override
    public final boolean equals(Object o){
        if(this == o){
            return true;
        }

        if(o == null){//为空返回false
            return false;
        }

        if(o instanceof Employee){
            return false;
        }
        Employee tmp = (Employee) o;
        return this.name == tmp.name && this.id == tmp.id;
    }
}

现在我们就大功告成了!!!



equals重写的优化

在equals定义的开始我们往往先看一下这俩对象是不是引用了一个对象(即公用一个地址),如果是一个引用对象那理所应当直接返回true。所以我们可以加上以下代码:

if(this == o){
            return true;//如果为同一个引用返回true
        }



总结

这里博主就把《Java核心技术 卷一》里的总结直接照搬过来了,因为它在这里的总结比我之前写的好多了。。

  1. 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
  2. 检测this与otherObject是否引用同一个对象:这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。
  3. 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。
  4. 比较this与otherObject是否属于同一个类。如果所有的子类都拥有统一的语义,就使用instanceof检测,而且最好用final修饰;如果equals的语义在每个子类中有所改变,就使用getClass检测。
  5. 将otherObject转换为相应的类类型变量
  6. 现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals比较对象域。如果所有的域都匹配,就返回true;否则返回false。如果在子类中重新定义equals,就要在其中包含调用super.equals(other)。



版权声明:本文为m0_61016568原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。