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

小心!Objects.equals有坑

时间:2023-03-22 13:10:20 科技观察

前言最近在review别人的代码的时候,发现同事在某个业务场景下使用Objects.equals方法判断两个值是否相等,返回的结果是与预期不符,这引起了我的兴趣。本来以为判断结果会返回true,结果居然返回false。记得很久以前用Objects.equals方法踩过类似的坑,所以有必要把这个问题记录下来分享给大家。这是怎么回事?1、案发现场假设有这样一个需求:判断当前登录的用户,如果是我们指定的系统管理员,则发送邮件。系统管理员没有特殊字段标识,他的用户id=888,在开发、测试、生产环境中该值相同。这个需求真的很容易实现:UserInfouserInfo=CurrentUser.getUserInfo();if(Objects.isNull(userInfo)){log.info("请先登录");return;}if(Objects.equals(userInfo.getId(),888)){sendEmail(userInfo):}从当前登录用户的上下文中获取用户信息,进行判断,如果是用户信息则直接返回是空的。如果获取到的用户信息不为空,则判断用户id是否等于888,若等于888,则发送邮件。如果不等于888,什么也不做。当我们用id=888的系统管理员账号登录,进行相关操作,满怀期待地准备接收邮件时,却发现收件人孤单。后来发现UserInfo类是这样定义的:@DatapublicclassUserInfo{privateLongid;私有字符串名称;私人整数年龄;privateStringaddress;}这时候可能有朋友会说:我看不出有什么问题。但我想说的是这段代码确实有问题。有什么问题?答:UserInfo类的成员变量id=888是Long类型,而Objects.equals方法右边的888是int类型。两者不一致,导致返回结果为false。这是什么原因?答:女士们,先生们,请放心,稍后我会详细解释。2.判断相等的方法让我们一起回顾一下,以前判断两个值是否相等的方法都有哪些。2.1在使用==符号之前,判断两个值是否相等最快的方法是使用==符号。inta=1;intb=1;bytec=1;Integerd1=newInteger(1);Integerd2=newInteger(1);System.out.println(a==b);//结果:trueSystem.out.println(a==c);//结果:trueSystem.out.println(a==d1);//结果:trueSystem.out.println(d2==a);//结果:trueSystem.out.println(d1==d2);//Result:false不知道大家有没有发现java中的基本类型包括:int,long,short,byte,char,boolean,float,double,可以用==号判断值是否是平等的。如果有基本类型的包装类,比如:Integer,就用一个基本类型和包装类,用==号判断正确,返回true。Integer和int比较的时候会自动拆箱,也就是比较值是否相等。但是如果有两个包装类,比如:d1和d2,使用==符号的结果可能是false。比较两个Integer时,比较的是它们指向的引用(即内存地址)是否相等。还有一个有趣的现象:整数d3=1;整数d4=1;整数d5=128;整数d6=128;System.out.println(d3==d4);//结果:trueSystem.out.println(d5==d6);//result:false是一个Integer类型的参数,直接赋值比较。d3和d4的判断结果相等,d5和d6的判断结果不相等。朋友,你的下巴掉了吗?答:因为Integer有常量池,-128~127直接Integer数据直接缓存到常量池中。所以1在常量池里,128不在。但是,新的Integer对象不适用于常量池。这从前面d1和d2例子的对比结果可以看出。接下来看字符串的判断:Stringe="abc";Stringf="abc";Stringg=newString("abc");Stringh=newString("abc");System.out。println(e==f);//结果:trueSystem.out.println(e==g);//结果:falseSystem.out.println(g==h);//result:false普通字符串变量,使用==号判断,也可以返回正确的结果。但是如果一个普通的字符串变量,用==符号判断新的字符串对象,就会返回false。这一点和我之前说的用一个基本类型和一个包装类,用==号判断结果是不一样的。字符串没有自动拆箱功能,需要特别注意。另外,当使用==符号判断这两个新的字符串对象时,也会返回false。2.2使用equals方法使用上面的==符号,可以快速判断8种基本数据类型是否相等。此外,还可以判断两个对象的引用是否相等。但是现在有个问题,它无法判断内存中两个对象的具体数据值是否相等,例如:Stringg=newString("abc");Stringh=newString("abc");系统输出。println(g==h);//result:false字符串对象g和h是两个不同的对象。当它们使用==符号来确定引用是否相等时,它们返回false。那么,当对象不同但数据值相同时,我们如何判断相等呢?答:使用equals方法。equals方法其实是Object类中的一个方法:publicbooleanequals(Objectobj){return(this==obj);}这个方法很简单,只判断两个对象的引用是否相等。显然,如果string类型直接使用父类(也就是Object类)的equals方法来判断对象不同但值相同,这是有问题的。因此,字符串(也就是String类)会重新equals方法:publicbooleanequals(ObjectanObject){if(this==anObject){returntrue;}if(anObjectinstanceofString){StringanotherString=(String)anObject;intn=值.长度;if(n==anotherString.value.length){charv1[]=value;charv2[]=anotherString.value;诠释我=0;while(n--!=0){如果(v1[i]!=v2[i])返回false;我++;}返回真;}}returnfalse;}还是会先判断两个对象引用是否相等,相等则返回true。接下来,将两个字符串逐个字符进行比较,只有所有字符都相等才返回true。nice,这样可以解决g和h判断的问题:Stringe="abc";Stringf="abc";System.out.println(e.equals(f));//Result:true由此可见,我们使用String类重写的equals方法判断两个字符串对象不同但值相同,会返回true。3.空指针异常前面我们已经知道,判断两个对象是否相等,可以使用==符号,也可以使用equals方法。但是如果你更深入地使用它们,你会发现一个问题,那就是:这两种判断值相等的方式可能会报空指针异常。先看==符号判断的情况:inta=1;整数b=新整数(1);整数c=null;System.out.println(a==b);//结果:trueSystem.out.println(a==c);//结果:当NullPointerExceptionint和Integer用==判断是否相等时,Integer会自动拆箱成int。但是由于c是在自动拆箱的过程中,所以需要给它赋默认值int0。而给一个空对象赋值0必然会导致空指针异常。接下来看equals方法:Stringe=null;Stringf="abc";System.out.println(e.equals(f));//Result:NullPointerException由于字符串对象e是空对象,直接调用它的equals方法会报空指针异常。那么,如何解决空指针问题呢?答:在代码中判断null。Stringe=null;Stringf="abc";System.out.println(equals(e,f));我们提取了一个新的equals方法:privatestaticbooleanequals(Stringe,Stringf){if(e==null){returnf==null;}returne.equals(f);}这个方法可以解决空指针问题,但是有没有办法封装一下,让它更通用,同时也适用于Integer或者其他类型的对象比较呢?答:有办法,继续看下去。4、Objects.equals的作用Objects类位于java.util包下,提供了很多对象操作的辅助方法。让我们关注它的equals方法:publicstaticbooleanequals(Objecta,Objectb){return(a==b)||(a!=null&&a.equals(b));}equals方法判断逻辑如下:该方法首先判断对象a和b的引用是否相等,相等则直接返回true。如果引用不相等,则判断a是否为空,如果a为空则返回false。如果a不为空,调用对象的equals方法进一步判断值是否相等。这个方法是如何使用的?整数=1;整数b=1;整数c=null;System.out.println(Objects.equals(a,c));//结果:falseSystem.out.println(Objects.equals(c,a));//结果:falseSystem.out.println(Objects.equals(a,b));//result:true从上面的列表中,使用Objects.equals方法比较两个对象是否相等,确实避免了空指针问题。但是有个问题:a==b是用来比较引用是否相等的。当b为空时,程序直接抛出空指针异常。Objects.equals方法也使用a==b来比较引用是否相等。为什么它不抛出异常?答:因为Objects类的equals方法使用Object类型来接收参数,它的默认值为null。对于类型转换,不需要像int类型的对象那样赋一个默认值0。从上面的理论我们可以看出,如果我们把代码改成这样,是不会抛出异常的:inta=1;整数c=null;System.out.println(equals(a,c));//结果:false定义了一个新的方法:privatestaticbooleanequals(Objecta,Objectb){returna==b;}执行后发现确实没有空指针。所以Objects.equals方法确实是比较两个对象是否相等的好方法。但是有坑,不信你继续往下看。5.Objects.equals的坑大家看到这里可能有点不耐烦了,到底是什么坑呢?废话不多说,直接上例子:Integera=1;longb=1L;System.out.println(Objects.equals(a,b));//result:false什么?返回结果是false?而如果直接用==号判断:Integera=1;长b=1L;System.out.println(a==b);//result:true再次返回true。a和b明明都是1,为什么要用Objects.equals方法判断不相等呢?这要从Integer的equals方法说起。其equals方法的具体代码如下:publicbooleanequals(Objectobj){if(objinstanceofInteger){returnvalue==((Integer)obj).intValue();}returnfalse;}首先判断参数obj是否为Integer类型,如果不是,直接返回false。如果是Integer类型,进一步判断int值是否相等。上面的例子中b是long类型,所以Integer的equals方法直接返回false。也就是说如果调用Integer的equals方法,输入的参数也必须是Integer类型,否则该方法会直接返回false。原坑在这里!!!其实如果把代码改成这样:Integera=1;longb=1L;System.out.println(Objects.equals(b,a));//Result:false执行结果也是false。因为Long的equals方法代码和前面的Integer类似:publicbooleanequals(Objectobj){if(objinstanceofLong){returnvalue==((Long)obj).longValue();}returnfalse;}也是判断如果不是Long类型,方法直接返回false。此外,Byte、Short、Double、Float、Boolean、Character也有类似的equals方法判断逻辑。可见,我们在使用Objects.equals方法判断两个值是否相等时,必须保证两个输入参数的类型一致。不然即使两个值一样,结果还是会返回false,坑很大。那么,如何解决上述问题呢?您可以将参数b的类型转换为int。整数a=1;长b=1L;System.out.println(Objects.equals(a,(int)b));//结果:true或将参数a的类型强制转换为long。Integera=1;longb=1L;System.out.println(Objects.equals(b,(long)a));//result:true某些情况下也可以直接通过==符号判断:Integer一=1;longb=1L;System.out.println(a==b);//result:true除了Objects.equals方法因为两个入参类型不同直接返回false外,封装了java的8种基本类型equals也会有同样的问题,男生需要特别注意。之前,如果直接使用java基本类型包装类的equals方法判断是否相等。整数a=新整数(1);长b=1L;System.out.println(a.equals(b));idea中,如果将鼠标放在equals方法上,会出现如下提示:这时你知道方法不对了,赶紧修复吧。但是如果直接使用包装类的equals方法,有一个问题就是可能有报空指针异常的风险。如果使用Objects.equals方法判断是否相等,idea中不会出现报错信息。另外我也测试了findBug和sonar等工具,也没有发现Objects.equals方法的两个参数类型不一致的问题。小伙伴们快看一下自己的代码,是不是踩坑了?常见的陷阱包括:Long类型和Integer类型的比较,例如:用户id的场景。Byte类型和Integer类型比较,比如:状态判断的场景。Double类型和Integer类型的比较,例如:金额为0的判断场景。