Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。Object 类中实现的 equals 方法将确定两个对象引用是否相等。这是一个合理的默认行为:如果两个对象引用相等,这两个对象肯定就相等。 对于很多类来说,这就足够了。例如,比较两个 PrintStream 对象是否相等并没有多大的意义。不过,经常需要基于状态监测对象的相等性,如果两个对象有相同的状态,才认为这两个对象是相等的。

例如,如果两个员工对象的姓名、薪水和雇用日期都一样,就认为它们是相等的。

class Employee
{
    private String name;
    private double salary;
    private LocalDate hireDay;

    ...

    @Override
    public boolean equals(Object otherObject)
    {
        // a quick test to see if the objects are identical
        if (this == otherObject) return true;

        // must return false if the explicit parameter is null
        if (otherObject == null) return false;

        // if the classes don't match, they can't be equal
        if (getClass() != otherObject.getClass())
            return false;
        
        // now we know otherObject is a non-null Employee
        Employee other = (Employee) otherObject;

        // test whether the fields have identical values
        return name.equals(other.name) 
                && salary == other.salary 
                && hireDay.equals(other.hireDay);
    }
}

public class Manager extends Employee
{
    ...

    @Override
    public boolean equals(Object otherObject)
    {
        if (!super.equals(otherObject)) return false;
        // super.equals checked that this and otherObject belong to the same class
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

为了防备 name 或 hireDay 可能为 null 的情况,需要使用 Objects.equals 方法。如果两个参数都为 null,Objects.equals(a, b) 调用将返回 true ; 如果其中一个参数为 null,则返回 false;否则,如果两个参数都不为 null,则调用 a.equals(b)。 利用这个方法,Employee.equals 方法的最后一条语句要改写为:

return Objects.equals(name, other.name)
    && salary == other.salary
    && Objects.equals(hireDay, other.hireDay);

在子类中定义 equals 方法时,首先调用超类的 equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。

public class Manager extends Employee
{
    ...
    public boolean equals(Object otherObject)
    {
        if (!super.equals(otherObject)) return false;
        // super.equals checked that this and otherObject belong to the same class
        Manager other = (Manager) otherObject;
        return bonus == other.bonus;
    }
}

1. 相等测试与继承

如果隐式和显式的参数不属于同一个类,equals 方法将如何处理呢?这是一个很有争议的问题。在前面的例子中,如果发现类不匹配,equals 方法就返冋 false。但是,许多程序员却喜欢使用 instanceof 进行检测:
if (!(otherObject instanceof Employee)) return false; 这样就允许 otherObject 属于一个子类,但是这种方法可能招致一些麻烦。正式因为这些麻烦,所以建议不要采用这种处理方式。

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。

这些规则当然合理。你肯定不希望类库实现者在查找数据结构中的一个元素时还要纠结调用 x.equals(y) 还是调用 y.equals(x) 的问题。

不过,就对称性规则来说,当参数不属于同一个类的时候会有一个微妙的结果。请看下面的调用:
e.equals(m); Manager 是 Employee 的子类,e 是一个 Employee 对象,m 是一个 Manager 对象,并且两个对象有相同的姓名、薪水和雇佣日期。如果在 Employee.equals 中用 instanceof 进行检测,则返回 true,然而这意味着反过来调用:
m.equals(e); 也需要返回 true,对称性不允许这个方法调用返回 false 或者抛出异常。
这就使得 Manager 类受到了束缚。这个类的 equals 方法必须愿意将自己与任何一个 Employee 对象进行比较,而不考虑 Manager 类特有的那部分信息!猛然间会让人感觉 instanceof 测试并不是那么好。

有些作者认为 getClass 检测是有问题的,因为它违反了替换原则。一个经常提到的例子,就是 AbstractSet 类的 equals 方法,它将检测两个集合是否有相同元素。AbstractSet 类有两个具体子类:TreeSet 和 HashSet,它们分别使用不同的算法查找集合元素。但无论集合采用何种方式实现,你肯定希望能够比较任意的两个集合。

不过,集合是非常特殊的一个例子,应该将 AbstractSet.equals 声明为 final,这是因为没有任何一个子类需要重新顶定义集合相等性的语义(事实上,这个方法并没有被声明为 final。这样做是为了让子类实现更高效的算法来完成相等性检测)。

就现在来看,有两种完全不同的情形:

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

在上面的 Employee 类和 Manager 类例子中,只要对应的字段相等,就认为两个对象相等。如果两个 Manager 对象的姓名、薪水和雇用日期均相等,而奖金不相等,就认为它们是不相同的,因此,我们要使用 getClass 检测。
但是,假设使用员工 ID 作为相等性检测标准,并且这个相等性概念使用于所有的子类,就可以使用 instanceof 检测,而且应该将 Employee.equals 声明为 final。

注释: 在标准 Java 库中包含 150 多个 equals 方法的实现,包括使用 instanceof 检测、调用 getClass 检测、捕获 ClassCastException 或者什么也不做等各种不同做法。可以查看 java.sql.Timestamp 类的 API 文档,在这里实现人员不无尴尬地指出,他们让自己陷入了困境。Timestamp 类继承自 java.util.Date,而后者的 equals 方法使用了一个 instanceof 测试,这样一来就无法覆盖 equals,使之同时做到对称且正确。

下面给出编写一个完美的 equals 方法的建议:

  1. 显式参数命名为 otherObject, 稍后需要将它转换成另一个名为 other 的变量。
  2. 检测 this 与 otherObject 是否引用同一个对象:
    if (this == otherObject) return true; 这条语句只是一个优化。实际上,这是一种经常采用的形式。因为检查身份要比逐个比较字段开销小。
  3. 检测 otherObject 是否为 null, 如果为 null, 返回 false。这项检测是很必要的。
    if (otherObject == null) return false;
  4. 比较 this 与 otherObject 的类。如果 equals 的语义可以在子类中改变,就使用 getClass 检测:
    if (getClass() != otherObject.getClass()) return false; 如果所有的子类都有相同的相等性语义,可以使用 instanceof 检测:
    if (!(otherObject instanceof ClassName)) return false;
  5. 将 otherObject 强制转换为相应的类类型变量:
    ClassName other = (ClassName) otherObject
  6. 现在根据相等性概念的要求来比较字段。使用 == 比较基本类型字段,使用 Objects.equals 比较对象字段。如果所有的字段都匹配,就返回 true;否则返回 false。
return field1 == other.field1
    && Objects.equals(field2, other.field2)
    && ...;
  1. 如果子类中重新定义 equals,就要在其中包含一个 super.equals(other) 调用。

提示: 对于数组类型的字段,可以使用静态的 Arrays.equals 方法检测相应的数组元素是否相等。

警告: 下面是实现 equals 方法时的一种常见的错误。

public class Employee
{
    public boolean equals(Employee other){
        return other != null
            && getClass() == other.getClass()
            && Objects.equals(name, other.name)
            && salary == other.salary
            && Objects.equals(hireDay, other.hireDay);
    }
    ...
}

这个方法声明的显式参数类型是 Employee。因此,它没有覆盖 Object 类的 equals 方法,而是定义了一个完全无关的方法。
为了避免发生这种错误,可以使用 @Override 标记要覆盖超类方法的那些子类方法:
@Override public boolean equals(Object other) 如果出现了错误,并且正在定义一个新方法,编译器就会报告一个错误。例如,假设将下面的声明添加到 Employee 类中:
@Override public boolean equals(Employee other) 就会看到一个错误报告,因为这个方法并没有覆盖超类 Object 中的任何方法。

java.util.Arrays 1.2

  • static boolean equals(xxx[] a, xxx[] b) 5

如果两个组长度相同,并且在对应的位置上数据元素也相同,将返回 true。数组的元素类型 xxx 可以是 Object、int、long、short、char、byte、boolean、float 或 double。

java.util.Objects 7

  • static boolean equals(Object a, Object b)

如果 a 和 b 都为null,返回 true;如果只有其中之一为 null,则返回 false;否则返回 a.equals(b)。