为什么在重写 equals方法的同时必须重写 hashcode方法
我们都知道Java语言是完全面向对象的,在java中,所有的对象都是继承于Object类。
其equals方法比较的是两个对象的引用指向的地址,hashcode是一个本地方法,返回的是对象地址值。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。
为何重写equals方法的同时必须重写hashcode方法呢
可以这样理解:重写了equals方法,判断对象相等的业务逻辑就变了,类的设计者不希望通过比较内存地址来比较两个对象是否相等,而hashcode方法继续按照地址去比较也没有什么意义了,索性就跟着一起变吧。
还有一个原因来源于集合。下面慢慢说~
举个例子:
在学校中,是通过学号来判断是不是这个人的。
下面代码中情景为学籍录入,学号123被指定给学生Tom,学号456被指定给学生Jerry,学号123被失误指定给Lily。而在录入学籍的过程中是不应该出现学号一样的情况的。
根据情景需求是不能添加重复的对象,可以通过HashSet实现。
publicclassTest{
publicstaticvoidmain(String[]args){
Studentstu=newStudent(123,"Tom");
HashSet<Student>set=newHashSet<>();
set.add(stu);
set.add(newStudent(456,"Jerry"));
set.add(newStudent(123,"Lily"));
Iterator<Student>iterator=set.iterator();
while(iterator.hasNext()){
Studentstudent=iterator.next();
System.out.println(student.getStuNum()+"---"+student.getName());
}
}
};
classStudent{
privateintstuNum;
privateStringname;
publicStudent(intstuNum,Stringname){
this.stuNum=stuNum;
this.name=name;
}
publicintgetStuNum(){
returnstuNum;
}
publicStringgetName(){
returnname;
}
@Override
publicbooleanequals(Objectobj){
if(this==obj)
returntrue;
if(objinstanceofStudent){
if(this.getStuNum()==((Student)obj).getStuNum())
returntrue;
}
returnfalse;
}
}
输出为:
123---Lily
456---Jerry
123---Tom
根据输出我们发现,再次将学号123指定给Lily居然成功了。到底哪里出了问题呢?
我们看一下HashSet的add方法:
publicbooleanadd(Ee){
returnmap.put(e,PRESENT)==null;
}
其实HashSet是通过HashMap实现的,由此我们追踪到HashMap的put方法:
publicVput(Kkey,Vvalue){
if(table==EMPTY_TABLE){
inflateTable(threshold);
}
if(key==null)
returnputForNullKey(value);
inthash=hash(key);
inti=indexFor(hash,table.length);
for(Entry<K,V>e=table[i];e!=null;e=e.next){
Objectk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
returnnull;
}
1.根据key,也就是HashSet所要添加的对象,得到hashcode,由hashcode做特定位运算得到hash码;
2.利用hash码定位找到数组下标,得到链表的链首;
3.遍历链表寻找有没有相同的key,判断依据是e.hash==hash&&((k=e.key)==key||key.equals(k))。在addLily的时候,由于重写了equals方法,遍历到Tom的时候第二个条件应该是true;但是因为hashcode方法还是使用父类的,故而Tom和Lily的hashcode不同也就是hash码不同,第一个条件为false。这里得到两个对象是不同的所以HashSet添加Lily成功。
总结出来原因是没有重写hashcode方法,下面改造一下:
publicclassTest{
publicstaticvoidmain(String[]args){
Studentstu=newStudent(123,"Tom");
HashSet<Student>set=newHashSet<>();
set.add(stu);
set.add(newStudent(456,"Jerry"));
set.add(newStudent(123,"Lily"));
Iterator<Student>iterator=set.iterator();
while(iterator.hasNext()){
Studentstudent=iterator.next();
System.out.println(student.getStuNum()+"---"+student.getName());
}
}
};
classStudent{
privateintstuNum;
privateStringname;
publicStudent(intstuNum,Stringname){
this.stuNum=stuNum;
this.name=name;
}
publicintgetStuNum(){
returnstuNum;
}
publicStringgetName(){
returnname;
}
@Override
publicbooleanequals(Objectobj){
if(this==obj)
returntrue;
if(objinstanceofStudent){
if(this.getStuNum()==((Student)obj).getStuNum())
returntrue;
}
returnfalse;
}
@Override
publicinthashCode(){
returngetStuNum();
}
}
输出:
456---Jerry
123---Tom
重写了hashcode方法返回学号。OK,大功告成。
有人可能会奇怪,e.hash==hash&&((k=e.key)==key||key.equals(k))这个条件是不是有点复杂了,我感觉只使用equals方法就可以了啊,为什么要多此一举去判断hashcode呢?
因为在HashMap的链表结构中遍历判断的时候,特定情况下重写的equals方法比较对象是否相等的业务逻辑比较复杂,循环下来更是影响查找效率。所以这里把hashcode的判断放在前面,只要hashcode不相等就玩儿完,不用再去调用复杂的equals了。很多程度地提升HashMap的使用效率。
所以重写hashcode方法是为了让我们能够正常使用HashMap等集合类,因为HashMap判断对象是否相等既要比较hashcode又要使用equals比较。而这样的实现是为了提高HashMap的效率。