详解Java中的相等测试equals与继承

Object中有一个equals方法,用来测试两个对象是否相等。该方法判断两个对象相等的条件是,两个对象的引用是否相等。如果两个对象的引用相等的话,那么毋庸置疑这两个对象一定相等。

但是,我们经常需要对对象的字段进行比较,如果两个对象具有相同的字段值,就认为这两个对象相等。比如,有两个员工姓名、年龄以及薪水相等的话,就认为他们相等。

下面我们来重写父类的equals方法,不再比较引用,而是比较几个字段值。

package com.studyjava.demo;

import java.util.Objects;

class Employee
{
    private String name;
    private int age;
    private double salary;

    public Employee (String name, int age, double salary)
    {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    public String getName ()
    {
        return this.name;
    }

    public int getAge ()
    {
        return this.age;
    }

    public double getSalary ()
    {
        return this.salary;
    }

    public boolean equals (Object other)
    {
        if (this == other) {
            return true;
        }

        if (other == null) {
            return false;
        }

        if (this.getClass() != other.getClass()) {
            return false;
        }

        Employee otherE = (Employee) other;

        if (Objects.equals(this.name, otherE.name) 
             && this.age == otherE.age
             && this.salary == otherE.salary) {
            return true;
        } else {
            return false;
        }
    }
}

这里我们刚开始使用了“==”来比较两个对象。补充“==”知识点:

  • java中的基本数据类型判断是否相等,直接使用"=="就行了,相等返回true,否则,返回false。
  • 但是java中的引用类型的对象比较变态,假设有两个对象obj1,obj2, obj1==obj2 判断是obj1,obj2这两个变量的引用是否相等,即它们所指向的是否为同一个对象(和Object.equals判断方法一致)。
  • 在Java API中,有些类重写了equals()方法,它们的比较规则是:当且仅当该equals方法参数不是 null,两个变量的类型、内容都相同,则比较结果为true。这些类包括:String、Double、Float、Long、Integer、Short、Byte、、Boolean、BigDecimal、BigInteger等等,太多太多了,但是常见的就这些了,具体可以查看API中类的equals()方法,就知道了。

好了,现在我们来分析这个重写的equals方法。

  • 注意参数的类型必须是Object。如果写成equals(Employee other),则不会覆盖Object类的equals方法,而是定义了一个完全无关的方法。
  • 第一个if用了“==”来判断这两个变量是否引用同一对象。如果引用同一变量,那么肯定相等。
  • 第二个if用来判断other是否为null,如果为null,那么返回false。这是因为,this肯定不为null,如果为null的话就会抛出异常。
  • 第三个条件判断是否同属一个类,如果不是同属一个类,那么就需返回false。
  • 接着就需对字段值进行比较了。这里用了Objects.equals()来比较name属性,为什么不直接用this.name.equals(otherE.name)?这是防止this.name如果为null的话会抛出异常。

Object类中的equals方法用于检测一个对象是否等于另一个对象。

if (a.equals(b)) {......}

这里,为了防止a可能为null的情况,可以换成Objects.equals方法。如果a和b都是null,则Objects(a,b)将返回true,若其中一个为null,则返回false,若都不是null,则返回a.equals(b)。

下面,我们把Employee的子类Manager的equals方法也重写下:

package com.studyjava.demo;

class Manager extends Employee
{
    private double bonus;

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

    public double getSalary ()
    {
        return super.getSalary() + this.bonus;
    }

    public void setBonus (double bonus)
    {
        this.bonus = bonus;
    }

    public boolean equals (Object manager)
    {
        if (!super.equals(manager)) {
            return false;
        }

        Manager mng = (Manager) manager;
        return this.bonus == mng.bonus;
    }
}

相等测试与继承

如果a.equals(b),a和b不属于同一个类,但他们之间是派生关系,equals该如何处理呢?这是一个非常值得讨论的话题。争论的核心在于是用getClass()检测是否为同一个类,还是用intanceof检测是否为继承关系。

java官方对equals方法有一个对称性的要求,即a.equals(b)为true,那么b.equals(a)也应该为true。这是非常合理的,因为我们不想在使用equals时还需要考虑用a.equals(b)还是b.equals(a)。

假如在之前的例子中,将Employee中equals方法中的

this.getClass() != other.getClass()

修改为

other instanceof this

那么,如果e.equals(m)返回true,(e为Employee实例,m为Manager实例)。那么为满足对称性,要使m.equals(e)也返回true。这样就导致一个问题,Manager中的equals就收到了限制。该equals方法必须愿意将自己与任何一个Employee对象进行比较,而不考虑经理特有的那部分信息。

关于是使用intanceof还是使用getClass,下面给出最佳实践:

  • 如果子类可以由自己的相等性概念,则对称性需求将强制使用getClass检测。
  • 如果由超类决定相等性概念,那么就可以使用instanceof检测,这样就可以再不同子类的对象之间进行相等性比较。

在员工和经理的例子中,只要相应的字段相等,就认为两个对象相等。如果两个Manager对象的姓名、年龄以及薪水均相等,而奖金不相等,就认为它们是不同的,因此我们要使用getClass检测。

但如果假设使用员工的ID作为相等性检测标准,并且这个相等性概念适用于所有子类,就可以使用instanceof检测,而且应该将Employee.equals声明为final