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

关于equals和hashCode,看这篇文章真的够了!

时间:2023-03-13 08:09:37 科技观察

这几天一直在尝试搭建一个类似Lombok的注解代码生成工具。用过Lombok的朋友都知道Lombok可以通过注解自动为我们生成equals()和hashCode()方法,所以我也想实现这个功能,但是随着工作的深入,我发现自己对equals()和hashCode()的理解hashCode()实际上处于非常低的水平。因此,痛定思痛,深入研究后,才敢写这篇博客。1、Java中equals的含义首先要解释清楚。equals方法表示Java中的逻辑相等性。什么是逻辑平等?这就涉及到Java本身的语法特点了。我们知道Java中有==来判断基本数据类型是否相等,但是对于对象来说,==只能判断内存地址是否相等,也就是说是否是同一个对象:inta=10000;intb=10000;//对于基本数据类型,==可以判断逻辑相等System.out.println(a==b);IntegerobjA=10000;IntegerobjB=10000;IntegerobjAobjA1=objA;//对于类实例,==可以只判断是否是同一个实例(可以看做内存地址是否相等)System.out.println(objA==objB);System.out.println(objA==objA1);注意:这里不讨论-128~127机制的Integer缓存。结果很明显:但是明明objA和objB在逻辑上是相等的,为什么会返回false呢?这时,一个需求诞生了。对于Java中的对象,如何判断对象的逻辑相等性,那么如何实现,所以出现了equals()方法。IntegerobjA=10000;IntegerobjB=10000;IntegerobjAobjA1=objA;//对于对象实例,equals可以判断两个对象逻辑上是否相等System.out.println(objA.equals(objB));Integer类重写了equals()方法,结果也很明显:所以如果我们自己创建一个类,要实现两个实例逻辑上是否相等,就需要重写他的equals()方法。//重写equals方法的类if(o==null||getClass()!=o.getClass())returnfalse;GoodExamplethat=(GoodExample)o;returnage==that.age&&Objects.equals(name,that.name);}}//否权重类staticclassBadExample{privateStringnakeName;privateintage;publicBadExample(StringnakeName,intage){this.nakeName=nakeName;this.age=age;}}publicstaticvoidmain(String[]args){System.out.println(newGoodExample("Richard",36).equals(newGoodExample("Richard",36)));System.out.println(newBadExample("Richard",36).equals(newBadExample("Richard",36)));}我相信你知道什么结果是:2、hashCode在Java中的作用网上有很多博客混淆了hashCode()和equals(),但实际上hashCode()就是它的字面意思,代表这个对象的哈希码。但是为什么JavaDoc会明确告诉我们hashCode()和equals()要一起重写呢?原因是,在Java自带的容器HashMap和HashSet中,都需要通过对象的hashCode()和equals()方法进行判断,然后插入删除元素。这个我们稍后再说。那么我们单独看一下hashCode()。为什么HashMap需要使用hashCode?这就涉及到了HashMap的底层数据结构——哈希表的原理:HashMap存储数据的底层结构其实是一个哈希表(也叫哈希表),哈希表将元素映射到数组的指定下标通过一个哈希函数Location,在Java中,这个哈希函数其实就是hashCode()方法。例如:HashMapmap=newHashMap<>();map.put("cringkong",newGoodExample("jack",10));map.put("cricy",newGoodExample("lisa",12));System.out.println(map.get("cricy"));在HashMap中存储时,HashMap会使用字符串“cringkong”和“cricy”的hashCode()映射到数组Location的指定下标,至于如何映射,后面会讲到。好了,现在我们明白为什么要设计hashCode()了,那我们来做个实验://科学设计的hashCodeclassstaticclassGoodExample{privateStringname;privateintage;publicGoodExample(Stringname,intage){this.name=name;this.age=age;}@OverridepublicinthashCode(){returnObjects.hash(name,age);}}//不科学的hashCode写法staticclassBadExample{privateStringnakeName;privateintage;publicBadExample(StringnakeName,intage){this.nakeName=nakeName;this.age=age;}@OverridepublicinthashCode(){//这里不使用returnnakeName.hashCode();}}这里有两个类,GoodExample类通过对类的所有字段进行hash得到hashCode,BadExample只使用一个对classes字段进行hash,我们看一下结果:System.out.println(newGoodExample("李老三",22).hashCode());System.out.println(newGoodExample("李老三",42).hashCode());System.out.println(newBadExample("王老五",50).hashCode());System.out.println(newBadExample("王老五",25).hashCode());可以看出GoodExample的hashCode()表示22岁和42岁的李老三不一样,但是BadExample认为50岁和25岁没有区别——老王老五。也就是说在HashMap中,两个李老三会放在不同的数组下标位置,两个王老五会放在同一个数组下标位置。PS:两个hashCode相等的对象在逻辑上不一定相等,两个hashCode相等的对象一定相等!3、为什么hashCode和equals要一起重写?刚才我们知道equals()用来判断对象在逻辑上是否相等,hashCode()用来获取一个对象的hash值,同时用来获取HashMap中的数组下标位置.那么为什么很多地方都说hashCode()和equals()要一起重写呢?很明显,数组下标可以通过对象的hashCode来定位,那我们为什么不直接把对象存进去取出来呢?答案是这样的:无论哈希函数设计得多么好,也会存在哈希冲突。什么是哈希冲突?比如我设计了这样一个哈希函数:/***硬核哈希函数,哈希规则是将传入字符串的第一个字符转换成ASCII值**@paramstring需要被哈希的字符串*@returnhashvalueofthestring*/privatestaticinthardCoreHash(Stringstring){returnstring.charAt(0);}我们来测试硬核哈希函数的哈希效果:System.out.println(hardCoreHash("fish"));System.out。println(hardCoreHash("cat"));System.out.println(hardCoreHash("fuck"));可以看到“fish”和“fuck”之间存在哈希冲突,这是我们不想看到的。一旦发生哈希冲突,我们的哈希表就需要解决哈希冲突。一般的解决方案是:开发寻址方法(线性检测和散列、二次检测和散列、伪随机检测和散列)然后散列方法和链地址方法建立一个公共溢出区。这些都是数据结构课本上的东西,就不细说了。不懂的同学可以自行搜索!正如我之前所说,无论哈希函数设计得多么好,都会存在哈希冲突。Java中的hashCode()本身就是一个hash函数,难免会出现hash冲突,这就更让一些程序害怕了。编写一些硬核哈希函数。既然有hash冲突,就要解决。HashMap使用链地址法解决:(盗图。。。这里有一个极端的情况,如何判断两个逻辑上相等的对象是否重复写入,或者两个逻辑上不相等的对象之间存在hash冲突怎么办?很简单,不就是用equals()方法搞定的吗?我们之前说过,equals()方法就是用来判断两个对象逻辑上是否相等的啊!我们来看一个HashCode源码,简单提取key对应的value:意思很简单,先判断key的hashCode是否相等,如果不相等,说明key和数组中的object在逻辑上一定不相等,就不用判断了.如果相等,继续判断逻辑上是否相等,从而判断是否存在hash冲突,或者是否取key对应的值。所以说到这里,你应该明白为什么了equals()和hashCode的顺序是千位oftimes()一起改写吧。这个类的对象如果要作为HashMap的key,或者存储到HashSet中,这两个方法都要重写。其他情况可以自行考虑,但为了安全和方便,千万不要搞错。直接改写就好了。4.扩展:实现科学的哈希函数。科学哈希函数不得不说是经典的字符串哈希函数:DJB哈希函数,俗称Times33的哈希函数:unsignedinttime33(char*str){unsignedinthash=5381;while(*str){hash+=(hash<<5)+(*str++);}return(hash&0x7FFFFFFF);}该函数的实现思路是将当前的hash值乘以33(左移5位相当于乘以32,然后加上原值相当于乘以33),加上字符串当前位置的值(ASCII),然后hash值进入下一次迭代,直到字符字符串的最后一位,hash迭代完成后返回值。为什么是科学的?因为根据实验,这种方式的哈希值分布比较均匀,也就是最不容易发生哈希碰撞,计算速度也比较快。至于初始值5381是怎么来的呢?这也是在实验中发现的一个比较科学的数字。(怎么感觉是废话?)那么Java中hashCode()有默认实现吗?当然还有://Object类中的hashCode函数是native方法,JVM实现了publicnativeinthashCode();Object类作为所有类的父类,实现了native方法,也就是本地方法,我们看不到JVM的实现。String类默认重写了hashCode方法。我们来看看实现:publicinthashCode(){//初始值为0inth=hash;if(h==0&&value.length>0){charval[]=value;//31作为乘数,应该是叫做Timers31?for(inti=0;i