点击查看【music163】

前言

equals() 和 hashcode() 是 Object 类提供的用于比较对象的两个重要方法。

由于 Object 类是所有 Java 对象的父类,因此所有对象都继承了这两个方法的默认实现。

在本文中,我们将看到对 equals() 和 hashCode() 方法的详细描述,它们如何相互关联,以及我们如何在自己定义的类中实现这两个方法。

版本约定

Object 类中的 equals 方法是用来判断两个对象是否相等。在 Object 类中,通过判断两个对象的地址(引用)是否相等来判断它们是否相等的。

源码如下所示:

  1. public boolean equals(Object obj) {
  2. return (this == obj);
  3. }

通过上面的源码,我们知道 Object 提供的 equals 方法等价于“==”方法,对于大多数的业务类来说,这种判断并没有什么意义。例如,采用这种方式比较两个学生对象是否相等就完全没有意义。一般我们是通过学生的 id 来判断两个学生对象是否相等。

因此,我们在定义类的时候会重写 equals() 方法,如果两个对象的内容相等,则 equals() 方法返回 true,否则,返回 false。

equals 方法的实现机制

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。

下面我们通过案例来演示 equals() 方法的实现机制,例如,定义一个雇员(Employee)类,有三个字段:姓名,薪水,雇佣日期。如果两个雇员对象的姓名、薪水、雇佣日期都一样,就认为它们是相等的。

  1. public class Employee {
  2. private String name;
  3. private int salary;
  4. private Date hireDay;
  5. @Override
  6. public boolean equals(Object otherObject) {
  7. // a quick test to see if the objects are identical
  8. if (this == otherObject) return true;
  9. // must return false if the explicit parameter is null
  10. if (otherObject == null) return false;
  11. // if the classes don't match, they can't be equal
  12. if (getClass() != otherObject.getClass()) return false;
  13. // now we know otherObject is a non-null Employee
  14. Employee other = (Employee) otherObject;
  15. // test whether the fields have identical values
  16. return salary == other.salary
  17. && Objects.equals(name, other.name)
  18. && Objects.equals(hireDay, other.hireDay);
  19. }
  20. // get set function
  21. }

新增一个雇员类的子类(Manager)用来描述经理角色,Manager 新增了一个特有的字段:bonus。Manager 类也需要定义一个 equals() 方法。

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

  1. public class Manager extends Employee {
  2. private int bonus;
  3. @Override
  4. public boolean equals(Object otherObject) {
  5. if (!super.equals(otherObject)) return false;
  6. // super.equals checked that this and otherObject belong to the same class
  7. Manager other = (Manager) otherObject;
  8. return bonus == other.bonus;
  9. }
  10. // get set function
  11. }

因为 Employee 类有一个继承类 Manager,我们在使用 equals 方法的时候,可能存在被比较的类和比较的类不属于同一个类的情况。

在前面的例子中,如果发现类型不匹配,equals 方法就返回 false。但是,在有些情况下,我们希望能够实现跨类型比较,比如父类和子类比较,子类与子类比较,这个要如何实现呢?

一般我们会想到使用 instanceof 代替上面的类型匹配操作。

  1. if (!(otherObject instanceof Employee)) return false;

子类实例 instanceof 父类,返回 true。所以,上面的方法看似解决了 otherObject 是子类的情况,但是它会带来其他的问题。

参考 equals 的具有的特性,上面的改动不能满足对称性,比如:e.equals(m),这里的 e 是一个 Employee 对象,m 是一个 Manager 对象,并且两个对象具有相同的姓名、薪水和雇佣日期。如果在 Employee.equals 中用 instanceof 进行检测,则返回 true,然而这意味着反过来调用:m.equals(e),也需要返回 true。对称性不允许这个方法调用返回 false,或者抛出异常。

这就使得 Manager 类受到了束缚。这个类的 equals 方法必须能够用自己与任何一个 Employee 对象进行比较,而不考虑经理拥有的那部分特有信息。

针对上面的问题,我们分为如下两种情况:

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

在雇员和经理的例子中,只要对应的域相等,就认为两个对象相等。如果两个 Manager 对象所对应的姓名、薪水和雇佣日期均相等,而奖金不相等,就认为它们是不相同的,因此,可以使用 getClass 检测。

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

下面给出编写 equals 方法建议(假设传入的参数命名为 otherObject):

  1. 检测 this 与 otherObject 是否引用同一个对象:if (this == otherObject) return true;
  2. 检测 otherObject 是否为 null,如果为 null,返回 false:if (otherObject == null) return false;
  3. 比较 this 与 otherObject 是否属于同一个类,这里分为两种情况:
    1. 如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:if (getClass() != otherObject.getClass()) return false;
    2. 如果所有的子类都拥有统一的语义,就使用 instanceof 检测:if (!(otherObject instanceof ClassName)) return false;,equals 方法只定义在父类上,且声明为 final。
  4. 将 otherObject 转换为相应的类类型变量:ClassName other = (ClassName) otherObject;
  5. 现在开始对所有需要比较的域进行比较。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true;否则返回 false。

    1. return fieldl == other.field1
    2. && Objects.equa1s(fie1d2, other.field2)
    3. && ...;
  6. 如果在子类中重新定义 equals,就要在其中包含调用 super.equals(other)。

    hashCode 方法

hashCode() 的作用是获取哈希码,也称为散列码,它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

由于 hashCode() 方法定义在 Object 类中, 因此每个对象都有一个默认的散列码,其值为对象的存储地址。

虽然,每个 Java 类都包含 hashCode() 函数。但是,仅仅当创建并使用某个类的散列表时,该类的 hashCode() 才有用,因为它需要确定该类的每一个对象在散列表中的位置,其它情况,比如只是创建类的单个对象,创建类的对象数组,类的 hashCode() 就没有什么作用了。

上面的散列表,指的是 Java 集合中本质是散列表的类,如 HashMap,Hashtable,HashSet。

equals() 方法和 hashCode() 方法都是用来比较两个对象是否相等的。那为什么散列表既需要 equals() 方法又需要 hashCode() 方法呢?

  • 因为重写的 equals() 方法相对复杂,效率比较低,而利用重写的 hashCode() 进行对比,则只要比较生成的 hash 值就可以了,效率很高。
  • 又因为 hashCode() 并不完全可靠,有时候不同的对象生成的 hashCode() 也会一样,所以 hashCode() 方法只能说是大部分时候可靠,并不是绝对可靠,所以还需要 equals() 方法进一步比较。

比如在 HashSet 里要求对象不能重复,它内部必然要对添加进去的每个对象进行对比,它的对比规则就是,先比较两个对象的 hashCode(),如果 hashCode() 相同,再用 equals() 验证,如果 hashCode() 都不同,则肯定不同,这样对比的效率就很高了。

所以,我们得出如下结论:

  • equals() 相等的两个对象,他们的 hashCode() 值肯定相等,也就是说用 equals() 对比是绝对可靠的;
  • hashCode() 相等的两个对象,他们的 equals() 不一定相等,也就是 hashCode() 不是绝对可靠的。

当然,前提是需要 hashCode() 来判断对象是否相等,如果只是我们自己调用 equals() 方法判断两个对象是否相等,hashCode() 方法就不在考虑内了。

一般情况下,我们在设计新的类的时候,如果需要重写 equals() 方法,强制同时重写 equals() 和 hashCode() 两个方法的,因为我们不确定该类是否会被添加到 HashMap,Hashtable,HashSet 等散列表的类中。

如果只是重写了 equals() 方法,而不重写 hashCode() 方法,那么在使用 HashSet 等散列表的时候,hashCode() 不相等,只是 equals() 方法相等,对象仍然会被判断为不相等,在 HashSet 中就存储了两个实际应该是相同的对象。所以阿里巴巴开发手册也规定了只要重写 equals() 方法,就必须重写 hashCode() 方法。

如何重写 hashCode() 方法?

equals() 方法与 hashCode() 方法的定义必须一致:如果 x.equals(y) 返回 true,那么 x.hashCode() 就必须与 y.hashCode( ) 具有相同的值。

比如上面的 Employee 类中,equals() 方法最终是通过 name,salary,hireDay 三个字段来确定两个对象是否相等的,Employee 类的 hashCode() 方法可以写成如下形式:

  1. @Override
  2. public int hashCode() {
  3. return Objects.hash(name, salary, hireDay);
  4. }

Objects 提供的 hash() 方法可以组合这三个参数的散列码。

如果只有一个参数建议使用 null 安全的方法 Objects.hashCode(),如果其参数为 null,这个方法会返回 0,否则返回对参数调用 hashCode 的结果。比如只判断 id 字段,就可以写成如下形式:

  1. @Override
  2. public int hashCode() {
  3. return Objects.hashCode(id);
  4. }

案例

最后给出上面例子的完整写法,仅作参考。

  1. 子类拥有自己的相等概念,在不同子类的对象之间不能进行相等的比较。 ```java public class Employee {

    private String name;

    private int salary;

    private Date hireDay;

    @Override public boolean equals(Object otherObject) {

    1. // a quick test to see if the objects are identical
    2. if (this == otherObject) return true;
    3. // must return false if the explicit parameter is null
    4. if (otherObject == null) return false;
    5. // if the classes don't match, they can't be equal
    6. if (getClass() != otherObject.getClass()) return false;
    7. // now we know otherObject is a non-null Employee
    8. Employee other = (Employee) otherObject;
    9. // test whether the fields have identical values
    10. return salary == other.salary
    11. && Objects.equals(name, other.name)
    12. && Objects.equals(hireDay, other.hireDay);

    }

    @Override public int hashCode() {

    1. return Objects.hash(name, salary, hireDay);

    }

    // get set function }

public class Manager extends Employee {

  1. private int bonus;
  2. @Override
  3. public final boolean equals(Object otherObject) {
  4. if (!super.equals(otherObject)) return false;
  5. // super.equals checked that this and otherObject belong to the same class
  6. Manager other = (Manager) otherObject;
  7. return bonus == other.bonus;
  8. }
  9. @Override
  10. public int hashCode() {
  11. return Objects.hash(super.hashCode(), bonus);
  12. }
  13. // get set function

}

  1. 2. 由父类决定相等的概念,可以在不同子类的对象之间进行相等的比较。建议 equals() hashCode() 方法都设置成 final,防止子类篡改,造成 equals() 不能满足对称性。
  2. ```java
  3. public class Employee {
  4. private String name;
  5. private int salary;
  6. private Date hireDay;
  7. @Override
  8. public final boolean equals(Object otherObject) {
  9. // a quick test to see if the objects are identical
  10. if (this == otherObject) return true;
  11. // must return false if the explicit parameter is null
  12. if (otherObject == null) return false;
  13. // if the classes don't match, they can't be equal
  14. if (!(otherObject instanceof Employee)) return false;
  15. // now we know otherObject is a non-null Employee
  16. Employee other = (Employee) otherObject;
  17. // test whether the fields have identical values
  18. return salary == other.salary
  19. && Objects.equals(name, other.name)
  20. && Objects.equals(hireDay, other.hireDay);
  21. }
  22. @Override
  23. public final int hashCode() {
  24. return Objects.hash(name, salary, hireDay);
  25. }
  26. }

转载

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/ypci2g 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。