当前位置: 首页 > 后端技术 > Java

面试官:为什么重写equals的时候一定要重写hashCode?

时间:2023-04-01 17:48:54 Java

重要提示:本文为博主《面试题精选-基础篇》系列之一,关注我查看更多面试题。Gitee面试题系列开源地址:https://gitee.com/mydb/interview本题难度:低共性:高equals方法和hashCode方法是Object类中的两个基本方法,它们共同判断两个对象是它相等。为什么要这样设计?究其原因,就在于“性能”二字。使用HashMap后,我们知道经过hash计算后,我们可以直接定位到某个值存放的位置,那么想象一下,如果现在要查询某个值是否在集合中呢?如果元素(存储位置)不是通过hash方法直接定位的,那么只能按照集合的顺序一个一个比较,这种顺序比较的效率明显低于hash定位方法。这是hash和hashCode的值。当我们比较两个对象是否相等时,可以先用hashCode来比较。如果比较的结果为真,那么我们可以用equals再次确认两个对象是否相等。如果比较的结果为真,则两个对象相等,否则认为两个对象不相等。这大大提高了对象比较的效率,这也是为什么Java设计使用hashCode和equals来确认两个对象是否相等的原因。那么为什么不直接使用hashCode来判断两个对象是否相等呢?这是因为不同对象的hashCode可能相同;但是hashCode不同的对象一定不相等,所以第一次使用hashCode可以快速判断对象是否相等。然而,即使了解了以上的基础知识,仍然无法解决本文的问题,即:为什么重写equals时还要重写hashCode?要弄清楚这个问题的根源,就得从这两种方法说起。1.equals方法Object类中的equals方法用于检测一个对象是否等于另一个对象。在Object类中,此方法将确定两个对象是否具有相同的引用。如果两个对象具有相同的引用,则它们必须相等。equals方法的实现源码如下:publicbooleanequals(Objectobj){return(this==obj);}通过上面的源码和equals的定义,我们可以看出在大多数情况下,平等的判断没有任何意义!例如,在Object中使用equals来比较两个自定义对象是否相等是完全没有意义的(因为无论对象是否相等,结果都是false)。这个问题可以用下面的例子来说明:publicclassEqualsMyClassExample{publicstaticvoidmain(String[]args){Personu1=newPerson();u1.setName("Java");u1.setAge(18);人u2=newPerson();u1.setName("Java");u1.setAge(18);//打印等于结果System.out.println("等于结果:"+u1.equals(u2));}}classPerson{私有字符串名称;私人年龄;publicStringgetName(){返回名称;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){返回年龄;}publicvoidsetAge(intage){this.age=age;}}上面程序的执行结果如下图所示:因此,一般情况下,如果我们要判断两个对象是否相等,就必须重写equals方法,这也是为什么我们需要重写equals方法的原因等于方法。2、hashCode方法hashCode翻译成中文就是哈希码,它是从一个对象派生出来的一个整数值,这个值可以是任意整数,包括正数或负数。需要注意的是,哈希码是不规则的。如果x和y是两个不同的对象,则x.hashCode()和y.hashCode()基本上永远不会相同;但如果a和b相等,则a.hashCode()必须等于b.hashCode()。Object中的hashCode源码如下:publicnativeinthashCode();从上面的源码可以看出,Object中的hashCode调用了一个(native)本地方法,返回一个int类型的整数。当然,这个整数可能是正数,也可能是负数。hashCode使用相等的值hashCode必须相同例子:publicclassHashCodeExample{publicstaticvoidmain(String[]args){Strings1="Hello";Strings2="你好";字符串s3="Java";System.out.println("s1hashCode:"+s1.hashCode());System.out.println("s2hashCode:"+s2.hashCode());System.out.println("s3hashCode:"+s3.hashCode());}}上面程序的执行结果如下图所示:不同的hashCode取值也可能相同例子:publicclassHashCodeExample{publicstaticvoidmain(String[]args){Strings1="Aa";字符串s2="BB";System.out.println("s1hashCode:"+s1.hashCode());System.out.println("s2hashCode:"+s2.hashCode());}}上面程序的执行结果,如下图所示:3.为什么要一起重写?接下来回到本文的主题,为什么重写equals的时候一定要重写hashCode呢?为了说明这个问题,我们需要从下面的例子说起。3.1SetSet集合的正常使用是用来保存不同的对象,相同的对象会被Set合并,最后留下唯一的一条数据。其正常用法如下:set.add("Java");set.add("Java");set.add("MySQL");set.add("MySQL");set.add("Redis");System.out.println("设置设置长度:"+set.size());System.out.println();//打印Set中的所有元素set.forEach(d->System.out.println(d));}}以上程序执行结果如下图所示:从上面的结果可以看出,重复数据已经被Set集合“合并”了,这也是Set集合最大的特点:去重.3.2Set集合的“变态”然而,如果我们在Set集合中存储一个自定义对象,只重写了equals方法,有趣的事情就会发生,如下代码所示:importjava.util.HashSet;importjava.util.Objects;importjava.util.Set;publicclassEqualsExample{publicstaticvoidmain(String[]args){//对象1Persionp1=newPersion();p1.setName("Java");p1.setAge(18);//对象2Persionp2=newPersion();p2.setName("Java");p2.setAge(18);//创建Set集合Setset=newHashSet();设置.添加(p1);设置.添加(p2);//打印Set中的所有数据set.forEach(p->{System.out.println(p);});}}classPersion{私有字符串名称;私人年龄;//只覆盖equals方法@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;//引用相等返回true//如果等于null,或者如果对象类型不同则返回falseif(o==null||getClass()!=o.getClass())returnfalse;//强制为自定义Persion类型Persionpersion=(Persion)o;//如果age和name都相等则返回truereturnage==period.age&&Objects.equals(name,period.name);}publicStringgetName(){返回名称;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){返回年龄;}publicvoidsetAge(intage){this.age=age;}@OverridepublicStringtoString(){return"Persion{"+"name='"+name+'\''+",age="+age+'}';}}上面程序的执行结果如下图所示:从上面的代码和上图可以看出,发现即使两个对象相等,Set集合也没有对两者进行去重和合并.这就是重写equals方法而不重写hashCode方法的问题。3.3解决“异常”为了解决上述问题,我们尝试在重写equals方法的同时重写hashCode方法。实现代码如下:importjava.util.HashSet;导入java.util.Objects;导入java。util.Set;publicclassEqualsToListExample{publicstaticvoidmain(String[]args){//对象1Personp1=newPersion();p1.setName("Java");p1.setAge(18);//对象2Persionp2=newPersion();p2.setName("Java");p2.setAge(18);//创建Set对象Setset=newHashSet();设置.添加(p1);设置.添加(p2);//打印Set中的所有数据set.forEach(p->{System.out.println(p);});}}classPersion{私有字符串名称;私人年龄;@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;//引用相等返回true//如果等于null,或者对象类型不同returnfalseif(o==null||getClass()!=o.getClass())returnfalse;//强制自定义Persion类型Persionpersion=(Pers离子)o;//如果age和name相等则返回truereturnage==period.age&&Objects.equals(name,period.name);}@OverridepublicinthashCode(){//比较name和age是否相等returnObjects.hash(name,age);}publicStringgetName(){返回名称;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){返回年龄;}publicvoidsetAge(intage){this.age=age;}@OverridepublicStringtoString(){return"Persion{"+"name='"+name+'\''+",age="+age+'}';}}上面程序的执行结果如下图所示:从上面的结果可以看出,我们把两个方法一起重写后,奇迹又发生了,Set集合恢复正常了。为什么?羊毛布?3.4原因分析出现上述问题的原因在于,如果只重写equals方法,那么默认情况下,Set在进行去重操作时,会先判断两个对象的hashCode是否相同。此时由于没有重写hashCode方法,所以会直接执行Object中的hashCode方法,而Object中的hashCode方法比较的是两个引用地址不同的对象,所以结果为false,那么equals方法不会需要执行,直接返回结果为false:两个对象没有相等,因此两个相同的对象被插入到Set集合中。但是如果在重写equals方法的时候hashCode方法也被重写了,那么在执行判断的时候就会执行重写的hashCode方法。此时比较的是两个对象的所有属性的hashCode是否相同,所以调用hashCode返回的结果为true,再调用equals方法,发现两个对象确实相等,所以它返回true,所以Set集合不会存储两个相同的数据,所以整个程序的执行是正常的。综上所述,hashCode和equals共同判断两个对象是否相等。之所以使用这种方法是为了提高程序插入和查询的速度。如果重写equals时hashCode没有重写,会导致在某些场景下,比如Set集合中存储了两个相等的自定义对象时,会导致程序执行异常。为了保证程序的正常执行,我们还需要重写equals。只需编写hashCode方法即可。关注公众号:Java面试真题解析,查看更多Java面试题。