当前位置: 首页 > 科技观察

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

时间:2023-03-20 13:05:26 科技观察

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的定义,我们可以看出在大多数情况下equals的判断是无意义的!例如,在Object中使用equals来比较两个自定义对象是否相等是完全没有意义的(因为无论对象是否相等,结果都是false)。这个问题可以用下面的例子来说明:publicclassEqualsMyClassExample{publicstaticvoidmain(String[]args){Personu1=newPerson();u1.setName("Java");u1.setAge(18);Personu2=newPerson();u1。setName("Java");u1.setAge(18);//打印等于结果System.out.println("equalsresult:"+u1.equals(u2));}}classPerson{privateStringname;privateintage;publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}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="Hello";Strings3="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";Strings2="BB";System.out.println("s1hashCode:"+s1.hashCode());System.out.println("s2hashCode:"+s2.hashCode());}}以上程序的执行结果如下图所示:3、为什么要一起重写?接下来回到本文的主题,为什么重写equals的时候一定要重写hashCode呢?为了解释这个问题,我们需要从下面的例子说起。3.1SetSet集合的正常使用是用来保存不同的对象,相同的对象会被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.Set;publicclassEqualsExample{publicstaticvoidmain(String[]args){//对象1Persionp1=newPersion();p1.setName("Java");p1.setAge(18);//对象2Persionp2=newPersion();p2.setName("Java");p2.setAge(18);//创建一个Set集合Setset=newHashSet();set.add(p1);set.add(p2);//打印Set中的所有数据set.forEach(p->{System.out.println(p);});}}classPersion{privateStringname;privateintage;//只覆盖equals方法@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;//如果引用相等则返回true//如果等于null,或者对象类型不同,则返回falseif(o==null||getClass()!=o.getClass())返回假;//强制自定义Persion类型Persionpersion=(Persion)o;//age和name相等则返回truereturnage==persion.age&&Objects.equals(name,persion.name);}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age=age;}@OverridepublicStringtoString(){return"Persion{"+"name='"+name+'\''+",age="+age+'}';}}以上程序执行结果如下图所示:从上面的代码和上图可以看出,即使两个对象相等,Set集合也没有对两者进行去重合并。这意味着重写了equals方法,但是没有重写hashCode方法的问题是。3.3解决“异常”为了解决上述问题,我们尝试在重写equals方法的同时重写hashCode方法。实现代码如下:importjava.util.HashSet;importjava.util.Objects;importjava.util.Set;publicclassEqualsToListExample{publicstaticvoidmain(String[]args){//Object1Persionp1=newPersion();p1.setName("java");p1.setAge(18);//对象2Persionp2=newPersion();p2.setName("Java");p2.setAge(18);//创建Set对象Setset=newHashSet();set.add(p1);set.add(p2);//打印所有SetDataset.forEach(p->{System.out.println(p);});}}classPersion{privateStringname;privateintage;@Overridepublicbooleanequals(Objecto){if(this==o)returntrue;//引用相等返回true//如果等于null,或者对象类型不同,返回falseif(o==null||getClass()!=o.getClass())returnfalse;//强制为自定义Persion类型Persionpersion=(Persion)o;//如果age和name相等,返回truereturnage==persion.age&&Objects.equals(name,persion.name);}@OverridepublicinthashCode(){//比较姓名和年龄是否相等returnObjects.hash(name,age);}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age=age;}@OverridepublicStringtoString(){return"Persion{"+"name='"+name+'\''+",age="+age+'}';}}上面程序的执行结果如下图所示:从上面的结果可以看出,当我们把两个方法重写在一起的时候,奇迹又发生了,集集又恢复正常了,这是为什么呢?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方法即可。