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

Java程序员99%都会踩的六大坑

时间:2023-03-20 16:54:46 科技观察

前言作为Java程序员,不知你有没有踩过一些基础知识的坑。有时候,你为了某个bug查了半天,最后发现是一个很低级的错误。有时,对于某些代码,这批数据功能正常,但是当一批数据发生变化时,就会出现异常。有时候,你可能看着某一行代码傻眼了,心里想:这行代码为什么会出错?今天就和大家聊聊99%的Java程序员都踩过或者即将踩过的6个陷阱。1.不知道大家有没有看到项目中用==号比较的坑。有同事用==号来比较Integer类型的两个参数。他们平等吗?不管怎样,我已经看到了,所以这个用法正确吗?我的回答要看具体的场景,不能说是对还是错。一些状态字段,如:orderStatus有:-1(未下单)、0(已下单)、1(已付款)、2(已完成)、3(已取消)、5种状态。这时如果用==判断是否相等:IntegerorderStatus1=newInteger(1);IntegerorderStatus2=newInteger(1);System.out.println(orderStatus1==orderStatus2);返回结果会是真的吗?答:是假的。可能有同学会反驳,Integer范围:-128-127不是有缓存吗?为什么是假的?我们先看看Integer的构造方法:它实际上并没有使用缓存。那么缓存用在什么地方呢?答案在valueOf方法中:如果把上面的判断改成这样:StringorderStatus1=newString("1");StringorderStatus2=newString("1");System.out.println(Integer.valueOf(orderStatus1)==Integer.valueOf(orderStatus2));返回结果会是真的吗?答:这是真的。我们需要养成良好的编码习惯,尽量少用==来判断两个Integer类型是否相等,只有在上述非常特殊的场景下才相等。而是使用equals方法判断:IntegerorderStatus1=newInteger(1);IntegerorderStatus2=newInteger(1);System.out.println(orderStatus1.equals(orderStatus2));运行结果为真。2、Objects.equals的坑假设有这样一个需求:判断当前登录的用户,如果是我们指定的系统管理员,则发送邮件。系统管理员没有特殊字段标识,他的用户id=888,在开发、测试、生产环境中该值相同。这个需求真的很容易实现:UserInfouserInfo=CurrentUser.getUserInfo();if(Objects.isNull(userInfo)){log.info("请先登录");return;}if(Objects.equals(userInfo.getId(),888L)){sendEmail(userInfo):}从当前登录用户的上下文中获取用户信息,进行判断,如果用户信息直接返回是空的。如果获取到的用户信息不为空,则判断用户id是否等于888,若等于888,则发送邮件。如果不等于888,什么也不做。当我们用id=888的系统管理员账号登录,进行相关操作,满怀期待地准备接收邮件时,却发现收件人孤单。后来发现UserInfo类是这样定义的:@DatapublicclassUserInfo{privateIntegerid;私有字符串名称;私人整数年龄;privateStringaddress;}这时候可能有朋友会说:我看不出有什么问题。但我想说的是这段代码确实有问题。有什么问题?让我们关注它的equals方法:publicstaticbooleanequals(Objecta,Objectb){return(a==b)||(a!=null&&a.equals(b));}equals方法判断逻辑如下:该方法首先判断对象a和b的引用是否相等,相等则直接返回true。如果引用不相等,则判断a是否为空,如果a为空则返回false。如果a不为空,调用对象的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。此外,Byte、Short、Double、Float、Boolean、Character也有类似的equals方法判断逻辑。常见的陷阱包括:Long类型和Integer类型的比较,例如:用户id的场景。Byte类型和Integer类型比较,比如:状态判断的场景。Double类型和Integer类型的比较,例如:金额为0的判断场景。如果想了解更多Objects.equals方法,可以看看我的另一篇文章《??Objects.equals有坑??》。3.BigDecimal的陷阱通常,我们将一些小数类型的字段(如:金额)定义为BigDecimal而不是Double,以避免精度损失。使用Double时,可能会出现这样的场景:doubleamount1=0.02;双倍金额2=0.03;System.out.println(金额2-金额1);正常情况下,预计amount2-amount1应该等于0.01,但是执行结果是:0.009999999999999998实际结果比预期的要少。Double类型的两个参数相减会转换成二进制,因为Double的有效位数是16位,所以会出现十进制位数存储不足,这种情况下会出错。常识告诉我们使用BigDecimal来避免精度损失。但是使用BigDecimal可以避免精度损失吗?答案是否定的。为什么?BigDecimalamount1=newBigDecimal(0.02);BigDecimalamount2=newBigDecimal(0.03);System.out.println(amount2.subtract(amount1));本例定义了两个BigDecimal类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。结果:0.0099999999999999984734433411404097569175064563751220703125不科学,为什么还是丢精度?Jdk中BigDecimal的构造方法有这样一段描述:大意是说这个构造函数的结果可能是不可预测的,创建的时候可能是0.1,但实际上是0.1000000000000000055511151231257827021181583404541015625。可见使用BigDecimal构造函数初始化对象也会丢失精度。那么,我们如何才能不失去精度呢?BigDecimalamount1=newBigDecimal(Double.toString(0.02));BigDecimalamount2=newBigDecimal(Double.toString(0.03));System.out.println(amount2.subtract(amount1));我们可以使用Double.toString方法,将double类型的小数进行转换,保证精度不丢失。其实还有更好的方法:BigDecimalamount1=BigDecimal.valueOf(0.02);BigDecimalamount2=BigDecimal.valueOf(0.03);System.out.println(amount2.subtract(amount1));使用BigDecimal.valueOf方法初始化BigDecimal类型的参数也可以保证精度不丢失。在新版阿里巴巴开发手册中,也推荐使用该方法创建BigDecimal参数。4.Java8filter的坑对于Java8中Stream的用法,想必大家都不陌生。通过对集合的Stream操作,我们可以实现:遍历集合、过滤数据、排序、判断、转换集合等,N多功能。这里我们重点介绍数据过滤。在Java8之前,我们一般会这样过滤数据:publicListfilterUser(ListuserList){if(CollectionUtils.isEmpty(userList)){}ListresultList=Lists.newArrayList();for(Useruser:userList){if(user.getId()>1000&&user.getAge()>18){resultList.add(user);}}returnresultList;}通常需要另一个集合协助这个功能。但是如果使用Java8的filter函数,代码会变得简洁很多,例如:);}returnuserList.stream().filter(user->user.getId()>1000&&user.getAge()>18).collect(Collectors.toList());}代码简化了很多,完美。但是如果修改过滤后的数据:ListuserList=queryUser();列表<用户>filterList=filterUser(userList);for(Useruser:filterList){user.setName(user.getName()+"test");}for(Useruser:userList){System.out.println(user.getName());}你可能只想修改过滤后的数据,但实际上,你会把修改后的元素数据放在一起。惊喜吗,惊喜吗?根本原因是:在过滤后的集合中,保存了对象的引用,而引用只有一份数据。也就是说,只要有一个地方修改了引用对象的成员变量的值,其他地方也会同步修改。如下图所示:5.自动拆箱的坑Java5之后,提供了自动装箱和自动拆箱的功能。自动装箱是指:JDK会自动将基本类型转换为包装类型。例如:整数整数=1;等价于:Integerinteger=newInteger(1);而自动拆箱的意思是:JDK会自动将包装类型转换为基本类型。例如:Integerinteger=newInteger(2);intsum=整数+5;等同于:Integerinteger=newInteger(2);intsum=integer.intValue()+5;但是在实际工作中,我们在使用自动拆箱时,往往会忘记判断null,导致NullPointerException异常。(1)操作很多时候,我们需要对传入的数据进行计算,例如:publicclassTest2{publicstaticvoidmain(String[]args){System.out.println(add(newInteger(1),newInteger(2)));}privatestaticIntegeradd(Integera,Integerb){returna+b;}}如果传入空值:System.out.println(add(null,newInteger(2)));会直接报错。(2)参数传递有时候,我们定义的一个方法是一个基本类型,但实际上传入的是一个包装类,如:publicstaticvoidmain(String[]args){Integera=newInteger(1);整数b=空;System.out.println(add(a,b));}privatestaticIntegeradd(inta,intb){returna+b;}如果add方法报告NullPointerException,你可能会感到困惑。int类型怎么会出现空指针异常呢?其实问题出在:Integer类型参数的实际输入值为null,JDK字段未装箱,调用了它的intValue方法。6、replace的坑很多时候我们在使用字符串的时候,想把字符串中的字符A比如:ATYSDFA*Y替换成字符B,第一个想到的可能就是使用replace方法。如果要用B替换所有的A,显然可以使用replaceAll方法,因为它非常直观,光看方法名就可以猜到它的用途。那么问题来了:replace方法会替换所有匹配的字符吗?jdk官方给出了答案。此方法替换每个匹配的字符串。既然replace和replaceAll都可以替换所有匹配的字符,那它们有什么区别呢?replace有两个重载方法。其中一个方法的参数:charoldChar和charnewChar,支持字符替换。source.replace('A','B')另一个方法的参数是:CharSequence目标和CharSequence替换,支持字符串替换。source.replace("A","B")和replaceAll方法的参数分别是:Stringregex和Stringreplacement,即基于正则表达式的替换。比如替换普通字符串:source.replaceAll("A","B")使用正则表达式替换(用C替换*):source.replaceAll("\\*","C")顺便说一下,用C替换*也可以使用replace方法实现:source.replace("*","C")各位看出两者的区别了吗?使用replace方法时不需要转义特殊字符。但是注意不要使用下面的写法:source.replace("\\*","C")这种写法会导致字符串不可替换。还有一个小问题,如果我只想替换第一个匹配的字符串怎么办?这时候可以使用replaceFirst方法:source.replaceFirst("A","B")说实话,这里的内容很基础,但是越是基础的东西越容易一不小心丢荆州,而且更容易踩坑。