前言
equals() 和 hashcode() 是 Object 类提供的用于比较对象的两个重要方法。
由于 Object 类是所有 Java 对象的父类,因此所有对象都继承了这两个方法的默认实现。
在本文中,我们将看到对 equals() 和 hashCode() 方法的详细描述,它们如何相互关联,以及我们如何在自己定义的类中实现这两个方法。
版本约定
- JDK 版本:1.8.0_231
- Java SE API Documentation:https://docs.oracle.com/javase/8/docs/api/
正文
equals 方法
Object 类中的 equals 方法是用来判断两个对象是否相等。在 Object 类中,通过判断两个对象的地址(引用)是否相等来判断它们是否相等的。
源码如下所示:
public boolean equals(Object obj) {
return (this == obj);
}
通过上面的源码,我们知道 Object 提供的 equals 方法等价于“==”方法,对于大多数的业务类来说,这种判断并没有什么意义。例如,采用这种方式比较两个学生对象是否相等就完全没有意义。一般我们是通过学生的 id 来判断两个学生对象是否相等。
因此,我们在定义类的时候会重写 equals() 方法,如果两个对象的内容相等,则 equals() 方法返回 true,否则,返回 false。
equals 方法的实现机制
Java 语言规范要求 equals 方法具有下面的特性:
- 自反性:对于任何非空引用 x,x.equals(x) 应该返回 true;
- 对称性:对于任何引用 x 和 y,当且仅当 y.equals(x) 返回 true,x.equals(y) 也应该返回 true;
- 传递性:对于任何引用 x、y 和 z,如果 x.equals(y) 返回 true,y.equals(z) 返回 true,x.equals(z) 也应该返回 true;
- 一致性:如果 x 和 y 引用的对象没有发生变化,反复调用 x.equals(y) 应该返回同样的结果;
- 对于任意非空引用 x,x.equals(null) 应该返回 false。
下面我们通过案例来演示 equals() 方法的实现机制,例如,定义一个雇员(Employee)类,有三个字段:姓名,薪水,雇佣日期。如果两个雇员对象的姓名、薪水、雇佣日期都一样,就认为它们是相等的。
public class Employee {
private String name;
private int salary;
private Date 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 salary == other.salary
&& Objects.equals(name, other.name)
&& Objects.equals(hireDay, other.hireDay);
}
// get set function
}
新增一个雇员类的子类(Manager)用来描述经理角色,Manager 新增了一个特有的字段:bonus。Manager 类也需要定义一个 equals() 方法。
在子类中定义 equals 方法时,首先调用超类的 equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
public class Manager extends Employee {
private int bonus;
@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;
}
// get set function
}
因为 Employee 类有一个继承类 Manager,我们在使用 equals 方法的时候,可能存在被比较的类和比较的类不属于同一个类的情况。
在前面的例子中,如果发现类型不匹配,equals 方法就返回 false。但是,在有些情况下,我们希望能够实现跨类型比较,比如父类和子类比较,子类与子类比较,这个要如何实现呢?
一般我们会想到使用 instanceof 代替上面的类型匹配操作。
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 对象进行比较,而不考虑经理拥有的那部分特有信息。
针对上面的问题,我们分为如下两种情况:
- 如果子类能够拥有自己的相等概念,则对称性需求将强制采用 getClass 进行检测。
- 如果由超类决定相等的概念,那么就可以使用 instanceof 进行检测,这样可以在不同子类的对象之间进行相等的比较。
在雇员和经理的例子中,只要对应的域相等,就认为两个对象相等。如果两个 Manager 对象所对应的姓名、薪水和雇佣日期均相等,而奖金不相等,就认为它们是不相同的,因此,可以使用 getClass 检测。
但是,假设使用雇员的 ID 作为相等的检测标准,并且这个相等的概念适用于所有的子类,就可以使用 instanceof 进行检测,并应该将 Employee.equals 声明为 final。
下面给出编写 equals 方法建议(假设传入的参数命名为 otherObject):
- 检测 this 与 otherObject 是否引用同一个对象:
if (this == otherObject) return true;
- 检测 otherObject 是否为 null,如果为 null,返回 false:
if (otherObject == null) return false;
- 比较 this 与 otherObject 是否属于同一个类,这里分为两种情况:
- 如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:
if (getClass() != otherObject.getClass()) return false;
- 如果所有的子类都拥有统一的语义,就使用 instanceof 检测:
if (!(otherObject instanceof ClassName)) return false;
,equals 方法只定义在父类上,且声明为 final。
- 如果 equals 的语义在每个子类中有所改变,就使用 getClass 检测:
- 将 otherObject 转换为相应的类类型变量:
ClassName other = (ClassName) otherObject;
现在开始对所有需要比较的域进行比较。使用 == 比较基本类型域,使用 equals 比较对象域。如果所有的域都匹配,就返回 true;否则返回 false。
return fieldl == other.field1
&& Objects.equa1s(fie1d2, other.field2)
&& ...;
如果在子类中重新定义 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() 方法可以写成如下形式:
@Override
public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
Objects 提供的 hash() 方法可以组合这三个参数的散列码。
如果只有一个参数建议使用 null 安全的方法 Objects.hashCode(),如果其参数为 null,这个方法会返回 0,否则返回对参数调用 hashCode 的结果。比如只判断 id 字段,就可以写成如下形式:
@Override
public int hashCode() {
return Objects.hashCode(id);
}
案例
最后给出上面例子的完整写法,仅作参考。
子类拥有自己的相等概念,在不同子类的对象之间不能进行相等的比较。 ```java public class Employee {
private String name;
private int salary;
private Date 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 salary == other.salary
&& Objects.equals(name, other.name)
&& Objects.equals(hireDay, other.hireDay);
}
@Override public int hashCode() {
return Objects.hash(name, salary, hireDay);
}
// get set function }
public class Manager extends Employee {
private int bonus;
@Override
public final 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;
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), bonus);
}
// get set function
}
2. 由父类决定相等的概念,可以在不同子类的对象之间进行相等的比较。建议 equals() 和 hashCode() 方法都设置成 final,防止子类篡改,造成 equals() 不能满足对称性。
```java
public class Employee {
private String name;
private int salary;
private Date hireDay;
@Override
public final 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 (!(otherObject instanceof Employee)) return false;
// now we know otherObject is a non-null Employee
Employee other = (Employee) otherObject;
// test whether the fields have identical values
return salary == other.salary
&& Objects.equals(name, other.name)
&& Objects.equals(hireDay, other.hireDay);
}
@Override
public final int hashCode() {
return Objects.hash(name, salary, hireDay);
}
}
转载
- Java 核心技术 卷1 基础知识 第10版
- Java hashCode() 和 equals()的若干问题解答
- 看似简单的hashCode和equals面试题,竟然有这么多坑!
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/ypci2g 来源:殷建卫 - 开发笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。