为什么要重写hashCode()和equals()方法
0.前言
在我使用输入这样private Set< Dog> dogs;Alibaba Java Coding Guidelines提示我参数类型Dog没有重写hashCode()和equals(),初学这个的我很懵逼,为啥这样提示我?在我一番搜集资料后才明白这样做的目的,同时也由衷的感觉这样做很有必要。
1.重写hashCode()和equals()方法
1.1提示原因
这种情况常见于一些以xx类(自定义类)为参数且其值只能唯一,比如Set< Student>中的Student。Set为集合,集合中不能有重复的元素,每一个元素是唯一的,以Student为例,这个集合里的每个Student类型的变量必须是唯一的。听到这可能有些懵,但不要紧,先接受,看完后面你就明白了。
提示让我们重写hashCode和equals,但怎么重写?在哪重写?首先明确从哪定义从哪重写,假设Student类在model中定义为含有学号和学校名两个属性,且通过学号和学校名两个变量唯一确定一个Student(一个学号可能在两个学校中有重复,一个学校中有很多学号不同的人,但一个学校中的学号不会有重复,因此需要通过学号和学校名唯一识别一个学生)。
1.2重写equals()方法
此时我定义一个Student的集合Set< Student>students,都知道集合里的元素不能重复,但怎么做到不能重复呢?Java中会调用equals方法(这是一个Objec类的方法,因此所有对象都有这样一个方法)来对新加如的元素如studen2,和之前集合中的元素如student1,进行比较确定是否为同一元素。如果不重写equal方法,只会比较student1和student2的地址是否相等。
//不重写equals方法的时,调用student1.equals(student2)方法的时equals源码
public boolean equals(Object obj) {
//this表示stude1,obj表示student2,==只比较地址
return (this == obj);
}
student1和student2为分别实例化的对象,所以地址肯定不等,即使student1和student2的学号和学校名相同,在这种情况下也是会认为不等的。因此我们需要重写equals方法使其具备能唯一识别学生。
代码如下:
@Override
public boolean equals(Object o) {
/*
* this表示调用方法的对象,o表示参数
* 如student1.equals(student2),student1为this,o为student2
* 先比较student1和student2的地址是否一致,如果一致就认为相等
*/
if (this == o) {
return true;
}
//判断o是不是Student类型的变量,如果不是直接返回false
if (!(o instanceof Student)) {
return false;
}
//把o强制转换成Student类型的变量
Student student = (Student) o;
/*
判断student1和student2的学号和学校名是否分别相等,有一个相等则返回false
id是int型,schoolName是String类型
这些基本类型变量都有一套完整的判断相等的代码,我们可以直接调用
*/
return Objects.equals(id, student.id) &&
Objects.equals(schoolName, student.schoolName);
}
每行代码的意思我都在注释里解释清楚了,看注释就可以了。至此我们已经完成了对equals()的重写。通过equals方法我们已经能够确定唯一的值了,那么我们为什么还要重写hashCode()呢?
1.3重写hashCode()方法
这里涉及一部分数据结构的知识,而且还涉及hashMap的底层实现,我只是略知一二,不是很懂,很深入的东西没有探讨,但足够理解了,如果没有数据结构基础的人,建议直接看代码,或者先补习一下数据结构的hash表的知识。这是因为在一些比如类型的变量如map中,我们常用hashMap()作为其实现类(Map<K,V> map=new HashMap()),这里面常用hashCode来比较两个K是否相等。默认的hashCode的返回值是一个将对象地址进行hash运算之后的结果值,所谓hash运算就是将一个数值输入之后,经过一个hash函数的运算最后的出一个结果,不同地址的对象的hashCode的结果不同,用来区分不同对象。因此我们还需要重写hashCode的方法,对不同地址的对象进行进一步区分。代码如下:
@Override
public int hashCode() {
int result = id.hashCode();
result = 31 * result + schoolName.hashCode();
//这是一个标准的模板,如果还需要name属性才能唯一确定,后面这样写,以此类推
// result = 31 * result + naame.hashCode();
return result;
}
这是一个标准的模板,之所以选择31是因为,源码中选择的值也为31,源码选择的原因是因为选择31是因为,首先这个值不能太小,太小容易产生重复值(学名叫冲突或者碰撞),比如52+4=62+2,太大值容易超出计算机能表示的范围,在多次校验中31还算合适,另外31是素数,用素数不易产生重复值(学名叫冲突或者碰撞),最终的出来的结果只能被素数本身和被乘数还有1来整除,比如用2的话就不一样了,还有一点是因为位移运算比较快(位移运算简单理解就是移动小数点,比如在十进制中,×10就可以表示为把小数点左移一位后的值,31=2*5-1,也就是说×31在计算机内部表示为左移5位之后-1,速度比乘快很多。最后一点也是因为上面的原因,很多设备(包括JVM,也就是Java虚拟机)都对31有特别的优化,速度比较快。
1.4使用idea自动生成重写的hashCode()和equals()
仔细观察可以发现重写equals和hashCode方法都是有固定的套路模板,这样的操作只能的idea可能可以帮我们自动生成。确实可以。在需要重写这两个方法的类中右键依次选择Generate,equals and hashCode,之后会跳出选择框,第一个是让你选择生成的模板,一般使用默认即可,第二个是选择在equals()方法中需要包含的属性(本例中的id和schoolName),第三是是选择在hashCode()方法中需要包含的属性(本例中的id和schoolName),有的会有第四个选择框,这是选择这些属性中需要设置为非空的属性值,之后点击finish即可。自动生成的代码和我自己写的会有不同,甚至不同模板的也不同,但原理都是一样的。
2.总结
这个提示感觉非常之有用,如果没有这个提示,我大概率是想不到重写equals()和hashCode()这两个方法的,到时候报错我都很难排查,而且这里面还涉及了一些数据结构的知识,让我对hash有了更深入的理解,越发感觉数据结构的重要性。同时也坚定了认真对待Alibaba Java Coding Guidelines,对于它提示的内容认真学习和改进。