equals方法:

一、根据Java1.8官方文档中对与equals方法的描述:​​​​​​​

equals方法属于Object类(根类)。指示其他对象是否“等于”这个对象。

等式方法实现了非空对象引用的等价关系:

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

2. 它是对称的:对于任何非空引用值x和y,x.equals(y)应该返回true,当且仅当y.equals(x)返回true。

3. 它是传递的:对于任何非空引用值x、y和z,如果x.equals(y)返回true,y.equals(z)返回true,则x.equals(z)应返回true。

4. 它是一致的:对于任何非空引用值x和y,x.equals(y)的多次调用一致返回true或始终返回false,前提是不修改用于对象等比较的信息。

5. 对于任何非空引用值x:x.equals(null)应返回false。

类Object的相等方法实现了对象上最具鉴别性的等价关系;也就是说,对于任何非空引用值x和y,此方法返回true,当且仅当x和y引用同一对象(x == y的值为true)。

请注意,每当此方法被重写时,通常都需要覆盖hashCode方法,以维护hashCode方法的一般合同,该合同规定相等对象必须具有相等的哈希代码。

二、根据源码:

*通过以上两点我们可以发现equals方法的本质是用于比较两个对象的内存地址是否相同。

让我们通过一个例子理解equals方法:

当o1/o2均为new出来的对象时,它们在堆内存中拥有两块不同的空间,此时o1和o2两个引用(存在于栈内存)分别指向两个不同的堆内存地址。这时调用equals方法返回的结果为false :

当o2为o1对象的引用复制时,o2引用同样指向o1对象,此时o1/o2两个引用对应相同的地址,而0x1001(原o2对象)对象由于不再有引用指向它,将被垃圾回收器销毁,此时调用equals方法的返回值为 true:

三、我们可以通过String类对equals方法的重写加深理解:

在String类对equals方法的重写源码中我们可以看到:首先对于地址进行判断,如果两个对象的地址相同,则返回 true,否则判断两个字符串的值是否相同,如果值相同则返回 true,反之亦然。


hashCode方法:

我们需要知道,在 Java 中的任何一个对象都包含一个 native 的 hashCode 方法:

*这是Object类对于hashCode方法的定义

在散列存储结构中,我们在新增元素时需要判断新增的元素是否已经存在于集合中,如果使用equals方法,效率过于低下,所以使用hashCode解决这个问题:如果集合中不存在与新增元素相同的hashCode值,则将新元素直接加入存储结构中;如果集合中存在与新增元素相同的hashCode值则调用equals方法进行进一步的比较,如果相同则覆盖,如果不相同则散列到其他地址,这使得调用equals方法的概率大大降低提高了存储速度。

hashCode就是对象的哈希码,默认情况下hashCode返回的是对象的存储地址映射成一个数值,也可以重写hashCode方法来实现自己的哈希码逻辑,通过哈希码可以提高查询的效率,主要用于在哈希表(散列表)结构中快速确定对象的存储地址,如HashMap、HashSet中。

需要注意的是:

1. 由于hashCode是JVM使用随机数生成,所以两个不同的对象可能会产生相同的hashCode值,可能造成哈希冲突问题。

2. 如果两个对象完全相同,即它们的内存地址相同,则它们的hashCode值一定相同


结合分析:

我们先建立一个只重写equals方法的Student类:

import java.util.Objects;public class Student {private String name;private int age;public Student(String name, int age) {this.name = name;this.age = age;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}/*Attention!*/@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Student student = (Student) o;return age == student.age && Objects.equals(name, student.name);}}

对于equals方法,我们先对内存地址进行判断,如果内存地址不同,再根据年龄和姓名的值进行判断。

在Main类中测试:

public class Main {public static void main(String[] args) {Student std1 = new Student("test", 18);Student std2 = new Student("test", 18);System.out.println("std1 & std2 are the same: " + std1.equals(std2));Set students = new HashSet();students.add(std1);students.add(std2);for (Student student : students) {System.out.println("name: " + student.getName() + ", age: " + student.getAge());}}}

创建std1和std2对象,使用构造方法赋初值(name = test,age = 18)。我们首先调用equals方法,判断两个对象是否相同,由于在Student类中重写了equals方法,故equals的返回值应为 true。我们继续将两个Student对象(std1、std2)加入到Set集合中(set集合为散列存储结构,并且不允许出现重复元素)。最后我们迭代这个Set集合(students),不断输出集合中的内容,由于两个对象先前调用过equals方法,说明这两个对象在逻辑上应该为相同的对象,不应该在集合中重复出现。

我们运行程序查看结果:

std1 & std2 are the same: truename: test, age: 18name: test, age: 18

两个相同的对象居然同时出现在了Set集合中!继续分析,我们使用new关键字创建了两个Student对象,根据先前的介绍,这两个对象在堆内存中位于不同的地址,而hashCode值又与地址有关,所以这两个对象的hashCode值不同,而在散列存储结构中添加元素时,首先判断新元素的hashCode值是否在集合中存在,当前情况下显然是不存在,Set集合将相同的“新”元素添加进了存储结构中。

故继续重写hashCode方法:(在Studen类中添加以下代码)

@Overridepublic int hashCode() {return Objects.hash(name, age);}

此处的hashCode方法使用Objects中提供的hash方法,其本质如下:

再次在Main类中测试:

std1 & std2 are the same: truename: test, age: 18

结果与逻辑相符合,问题成功解决了。


总结:

如果只重写equals方法,不重写hashCode方法,就有可能导致在 x.equals(y) 表达式成立的条件下,x 和 y 的 hashCode 值却不相同。此时这个只重写了 equals 方法的对象在使用散列集合进行存储的时候,由于散列集合使用 hashCode 来确定 key 的位置,如果存储两个完全相同的对象但是这两个对象有不同的hashCode值,就会出现两个相同的对象储存在散列集合的不同位置,违反了散列集合的规则,也会造成该类对象无法使用散列存储结构。