1.前言
中国有句老话叫"事不过三",指一个人犯了同样的错误,一次两次三次还可以原谅,超过三次就不可原谅了。有人指出这个“三”是虚数,用来泛指多次,所以"事不过三"不包括“三”。至于"事不过三"包不包括“三”,可能跟每个人的底线有关系,属于哲学范畴,不在本文的讨论范围之内。
写代码也是如此,同一个代码“坑”,踩第一次叫"长了经验",踩第二次叫"加深印象",踩第三次叫"不长心眼",踩三次以上就叫"不可救药"。在本文中,笔者总结了一些代码坑,描述了问题现象,进行了问题分析,给出了避坑方法。希望大家在日常编码中,遇到了这类代码坑,能够提前避让开来。1.对象比较方法JDK1.7提供的Objects.equals方法,非常方便地实现了对象的比较,有效地避免了繁琐的空指针检查。1.1.问题现象在JDK1.7之前,在判断一个短整型、整型、长整型包装数据类型与常量是否相等时,我们一般这样写:ShortshortValue=(short);System.out.println(shortValue==);//trueSystem.out.println(==shortValue);//trueIntegerintValue=;System.out.println(intValue==);//trueSystem.out.println(==intValue);//trueLonglongValue=L;System.out.println(longValue==);//trueSystem.out.println(==longValue);//true
从JDK1.7之后,提供了Objects.equals方法,并推荐使用函数式编程,更改代码如下:ShortshortValue=(short);System.out.println(Objects.equals(shortValue,));//falseSystem.out.println(Objects.equals(,shortValue));//falseIntegerintValue=;System.out.println(Objects.equals(intValue,));//trueSystem.out.println(Objects.equals(,intValue));//trueLonglongValue=L;System.out.println(Objects.equals(longValue,));//falseSystem.out.println(Objects.equals(,longValue));//false
为什么直接把==替换为Objects.equals方法会导致输出结果不一样?1.2.问题分析通过反编译第一段代码,我们得到语句"System.out.println(shortValue==);"的字节码指令如下:7getstaticjava.lang.System.out:java.io.PrintStam[22]10aload_1[shortValue]11invokevirtualjava.lang.Short.shortValue():short[28]14sipush17if_icmpneiconst_gotoiconst_invokevirtualjava.io.PrintStam.println(boolean):void[32]
原来,编译器会判断包装数据类型对应的基本数据类型,并采用这个基本数据类型的指令进行比较(比如上面字节码指令中的sipush和if_icmpne等),相当于编译器自动对常量进行了数据类型的强制转化。为什么采用Objects.equals方法后,编译器不自动对常量进行数据类型的强制转化?通过反编译第二段代码,我们得到语句"System.out.println(Objects.equals(shortValue,));"的字节码指令如下:7getstaticjava.lang.System.out:java.io.PrintStam[22]10aload_1[shortValue]11sipush14invokestaticjava.lang.Integer.valueOf(int):java.lang.Integer[28]17invokestaticjava.util.Objects.equals(java.lang.Object,java.lang.Object):boolean[33]20invokevirtualjava.io.PrintStam.println(boolean):void[39]
原来,编译器根据字面意思,认为常量默认基本数据类型是int,所以会自动转化为包装数据类型Integer。在Java语言中,整数的默认数据类型是int,小数的默认数据类型是double。下面来分析一下Objects.equals方法的代码实现:publicstaticbooleanequals(Objecta,Objectb){turn(a==b)
(a!=nulla.equals(b));}
其中,语句“a.equals(b)”将会使用到Short.equals方法。Short.equals方法的代码实现为:publicbooleanequals(Objectobj){if(objinstanceofShort){turnvalue==((Short)obj).shortValue();}turnfalse;}
通过代码实现分析:对应语句"System.out.println(Objects.equals(shortValue,));",因为Objects.equals的两个参数对象类型不一致,一个是包装数据类型Short,另一个是包装数据类型Integer,所以最终的比较结果必然是false。同样,语句“System.out.println(Objects.equals(intValue,));”,因为Objects.equals的两个参数对象类型一致,都是包装数据类型Integer且取值一样,所以最终的比较结果必然是true。1.3.避坑方法1、保持良好的编码习惯,避免数据类型的自动转化为了避免数据类型自动转化,更科学的写法是直接声明常量为对应的基本数据类型。第一段代码可以这样写:ShortshortValue=(short);System.out.println(shortValue==(short));//trueSystem.out.println((short)==shortValue);//trueIntegerintValue=;System.out.println(intValue==);//trueSystem.out.println(==intValue);//trueLonglongValue=L;System.out.println(longValue==L);//trueSystem.out.println(L==longValue);//true
第二段代码可以这样写:
ShortshortValue=(short);System.out.println(Objects.equals(shortValue,(short)));//trueSystem.out.println(Objects.equals((short),shortValue));//trueIntegerintValue=;System.out.println(Objects.equals(intValue,));//trueSystem.out.println(Objects.equals(,intValue));//trueLonglongValue=L;System.out.println(Objects.equals(longValue,L));//trueSystem.out.println(Objects.equals(L,longValue));//true
2、借助开发工具或插件,及早地发现数据类型不匹配问题在Eclipse的问题窗口中,我们会看到这样的提示:Unlikelyargumenttypeforequals():intseemstobeunlatedtoShortUnlikelyargumenttypeforequals():ShortseemstobeunlatedtointUnlikelyargumenttypeforequals():intseemstobeunlatedtoLongUnlikelyargumenttypeforequals():Longseemstobeunlatedtoint
通过FindBugs插件扫描,我们会看到这样的警告:CalltoShort.equals(Integer)inxxx.Xxx.main(String[])[Scariest(1),Highconfidence]CalltoInteger.equals(Short)inxxx.Xxx.main(String[])[Scariest(1),Highconfidence]CalltoLong.equals(Integer)inxxx.Xxx.main(String[])[Scariest(1),Highconfidence]CalltoInteger.equals(Long)inxxx.Xxx.main(String[])[Scariest(1),Highconfidence]
3、进行常规性单元测试,尽量把问题发现在研发阶段“勿以善小而不为”,不要因为改动很小就不需要进行单元测试了,往往Bug都出现在自己过度自信的代码中。像这种问题,只要进行一次单元测试,是完全可以发现问题的。2.三元表达式拆包三元表达式是Java编码中的一个固定语法格式:“条件表达式?表达式1:表达式2”。三元表达式的逻辑为:“如果条件表达式成立,则执行表达式1,否则执行表达式2”。
2.1.问题现象booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Doublesult=condition?value1*value2:value3;//抛出空指针异常
当条件表达式condition等于false时,直接把Double对象value3赋值给Double对象sult,按道理没有问题呀,为什么会抛出空指针异常(NullPointerException)?2.2.问题分析通过反编译代码,我们得到语句"Doublesult=condition?value1*value2:value3;"的字节码指令如下:17iload_1[condition]18ifeqaload_2[value1]22invokevirtualjava.lang.Double.doubleValue():double[24]25aload_3[value2]26invokevirtualjava.lang.Double.doubleValue():double[24]29dmul30gotoaload4[value3]35invokevirtualjava.lang.Double.doubleValue():double[24]38invokestaticjava.lang.Double.valueOf(double):java.lang.Double[16]41asto5[sult]43getstaticjava.lang.System.out:java.io.PrintStam[28]46aload5[sult]
在第33行,加载Double对象value3到操作数栈中;在第35行,调用Double对象value3的doubleValue方法。这个时候,由于value3是空对象null,调用doubleValue方法必然抛出抛出空指针异常。但是,为什么要把空对象value3转化为基础数据类型double?查阅相关资料,得到三元表达式的类型转化规则:若两个表达式类型相同,返回值类型为该类型;若两个表达式类型不同,但类型不可转换,返回值类型为Object类型;若两个表达式类型不同,但类型可以转化,先把包装数据类型转化为基本数据类型,然后按照基本数据类型的转换规则(byteshort(char)intlongfloatdouble)来转化,返回值类型为优先级最高的基本数据类型。根据规则分析,表达式1(value1*value2)计算后返回基础数据类型double,表达式2(value3)返回包装数据类型Double,根据三元表达式的类型转化规则判断,最终的返回类型为基础数据类型double。所以,当条件表达式condition等于false时,需要把空对象value3转化为基础数据类型double,于是就调用了value3的doubleValue方法抛出了空指针异常。可以用以下案例验证三元表达式的类型转化规则:booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Integervalue4=null;//返回类型为Double,不抛出空指针异常Doublesult1=condition?value1:value3;//返回类型为double,会抛出空指针异常Doublesult2=condition?value1:value4;//返回类型为double,不抛出空指针异常Doublesult3=!condition?value1*value2:value3;//返回类型为double,会抛出空指针异常Doublesult4=condition?value1*value2:value3;
2.3.避坑方法1、尽量避免使用三元表达式,可以采用if-else语句代替如果三元表达式中有算术计算和包装数据类型,可以考虑利用if-else语句代替。改写代码如下:booleancondition=false;Doublevalue1=1.0D;Doublevalue2=2.0D;Doublevalue3=null;Doublesult;if(condition){sult=value1*value2;}else{sult=value3;}
2、尽量使用基本数据类型,避免数据类型的自动转化如果三元表达式中有算术计算和包装数据类型,可以考虑利用if-else语句代替。改写代码如下:booleancondition=false;doublevalue1=1.0D;doublevalue2=2.0D;doublevalue3=3.0D;doublesult=condition?value1*value2:value3;
3、进行覆盖性单元测试,尽量把问题发现在研发阶段像这种问题,只要编写一些单元测试用例,进行一些覆盖性测试,是完全可以提前发现的。3.泛型对象赋值Java泛型是JDK1.5中引入的一个新特性,其本质是参数化类型,即把数据类型做为一个参数使用。3.1.问题现象在做用户数据分页查询时,因为笔误编写了如下代码:1、PageDataVO.java:/**分页数据VO类*/
GetterSetterToStringNoArgsConstructorAllArgsConstructorpublicclassPageDataVOT{/**总共数量*/privateLongtotalCount;/**数据列表*/privateListTdataList;}2、UserDAO.java:/**用户DAO接口*/
MapperpublicinterfaceUserDAO{/**统计用户数量*/publicLongcountUser(Param("query")UserQueryVOquery);/**查询用户信息*/publicListUserDOqueryUser(Param("query")UserQueryVOquery);}3、UserService.java:/**用户服务类*/
ServicepublicclassUserService{/**用户DAO*/AutowidprivateUserDAOuserDAO;/**查询用户信息*/publicPageDataVOUserVOqueryUser(UserQueryVOquery){ListUserDOdataList=null;LongtotalCount=userDAO.countUser(query);if(Objects.nonNull(totalCount)totalCount.