为什么在重写 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的效率。