写点什么

很不起眼的 6 个 bug,90% 的程序员就算写了 10 年代码也肯定都踩过!

作者:Java-fenn
  • 2022 年 9 月 25 日
    湖南
  • 本文字数:4499 字

    阅读完需:约 15 分钟

前言


作为 Java 程序员的你,不知道有没有踩过一些基础知识的坑。


有时候,某个 bug 查了半天,最后发现竟然是一个低级错误。


有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。


有时候,你可能会看着某行代码目瞪口呆,心里想:这行代码为什么会出错?


今天跟大家一起聊聊 99%的 Java 程序员踩过,或者即将踩的 6 个坑。


  1. 用==号比较的坑


不知道你在项目中有没有见过,有些同事 对 Integer 类型的两个 参数使用 ==号比较 是否相等?


反正我见过的,那么这种用法对吗?


我的回答是看具体场景,不能说一定对,或不对。


有些状态字段,比如:orderStatus 有:-1(未下单),0(已下单),1(已支付),2(已完成),3(取消),5 种状态。


这时如果用==判断是否相等:


Integer orderStatus1 = new Integer(1);Integer orderStatus2 = new Integer(1);System.out.println(orderStatus1 == orderStatus2);返回结果会是 true 吗?


答案:是 false 。


有些同学可能会反驳,Integer 中不是有范围是: -128-127 的缓存 吗?


为什么是 false?


先看看 Integer 的构造方法:


它其实并没有用到 缓存 。


那么缓存是在哪里用的?


答案在 valueOf 方法中:


如果上面的判断改成这样:


String orderStatus1 = new String("1");String orderStatus2 = new String("1");System.out.println(Integer.valueOf(orderStatus1) == Integer.valueOf(orderStatus2));返回结果会是 true 吗?


答案:还真是 true 。


我们要养成良好编码习惯,尽量少用==判断两个 Integer 类型数据是否相等,只有在上述非常特殊的场景下才相等。


而应该改成使用 equals 方法判断:


Integer orderStatus1 = new Integer(1);Integer orderStatus2 = new Integer(1);System.out.println(orderStatus1.equals(orderStatus2));运行结果为 true 。


  1. Objects.equals 的坑


假设现在有这样一个需求: 判断当前登录的用户,如果是我们指定的系统管理员,则发送一封邮件。 系统管理员没有特殊的字段标识,他的用户 id=888,在开发、测试、生产环境中该值都是一样的。


这个需求真的太容易实现了:


UserInfo userInfo = CurrentUser.getUserInfo();


if(Objects.isNull(userInfo)) {log.info("请先登录");return;}


if(Objects.equals(userInfo.getId(),888L)) {sendEmail(userInfo):}从当前登录用户的上下文中获取用户信息,判断一下,如果用户信息为空,则直接返回。


如果获取到的用户信息不为空,接下来判断用户 id 是否等于 888。


如果等于 888,则发送邮件。


如果不等于 888,则啥事也不干。


当我们用 id=888 的系统管理员账号登录之后,做了相关操作,满怀期待的准备收邮件的时候,却发现收了个寂寞。


后来,发现 UserInfo 类是这样定义的:


@Datapublic class UserInfo {private Integer id;private String name;private Integer age;private String address;}此时,有些小伙伴可能会说:没看出什么问题呀。


但我要说的是这个代码确实有问题。


什么问题呢?


下面我们重点看看它的 equals 方法:


public static boolean equals(Object a, Object b) {return (a == b) || (a != null && a.equals(b));}equals 方法的判断逻辑如下:


该方法先判断对象 a 和 b 的引用是否相等,如果相等则直接返回 true。


如果引用不相等,则判断 a 是否为空,如果 a 为空则返回 false。


如果 a 不为空,调用对象的 equals 方法进一步判断值是否相等。


这就要从 Integer 的 equals 方法说起来了。


它的 equals 方法具体代码如下:


public boolean equals(Object obj) {if (obj instanceof Integer) {return value == ((Integer)obj).intValue();}return false;}先判断参数 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 的判断场景。


  1. BigDecimal 的坑


通常我们会把一些小数类型的字段(比如:金额),定义成 BigDecimal ,而不是 Double ,避免丢失精度问题。


使用 Double 时可能会有这种场景:


double amount1 = 0.02;double amount2 = 0.03;System.out.println(amount2 - amount1);正常情况下预计 amount2 - amount1 应该等于 0.01


但是执行结果,却为:


0.009999999999999998 实际结果小于预计结果。


Double 类型的两个参数相减会转换成二进制,因为 Double 有效位数为 16 位这就会出现存储小数位数不够的情况,这种情况下就会出现误差。


常识告诉我们使用 BigDecimal 能避免丢失精度。


但是使用 BigDecimal 能避免丢失精度吗?


答案是否定的。


为什么?


BigDecimal amount1 = new BigDecimal(0.02);BigDecimal amount2 = new BigDecimal(0.03);System.out.println(amount2.subtract(amount1));这个例子中定义了两个 BigDecimal 类型参数,使用构造函数初始化数据,然后打印两个参数相减后的值。


结果:


0.0099999999999999984734433411404097569175064563751220703125 不科学呀,为啥还是丢失精度了?


Jdk 中 BigDecimal 的 构造方法 上有这样一段描述:


大致的意思是此构造函数的结果可能不可预测,可能会出现创建时为 0.1,但实际是 0.1000000000000000055511151231257827021181583404541015625 的情况。


由此可见,使用 BigDecimal 构造函数初始化对象,也会丢失精度。


那么,如何才能不丢失精度呢?


BigDecimal amount1 = new BigDecimal(Double.toString(0.02));BigDecimal amount2 = new BigDecimal(Double.toString(0.03));System.out.println(amount2.subtract(amount1));我们可以使用 Double.toString 方法,对 double 类型的小数进行转换,这样能保证精度不丢失。


其实,还有更好的办法:


BigDecimal amount1 = BigDecimal.valueOf(0.02);BigDecimal amount2 = BigDecimal.valueOf(0.03);System.out.println(amount2.subtract(amount1));使用 BigDecimal.valueOf 方法初始化 BigDecimal 类型参数,也能保证精度不丢失。在新版的阿里巴巴开发手册中,也推荐使用这种方式创建 BigDecimal 参数。


  1. Java8 filter 的坑


对于 Java8 中的 Stream 用法,大家肯定再熟悉不过了。


我们通过对 集合 的 Stream 操作,可以实现:遍历集合、过滤数据、排序、判断、转换集合等等,N 多功能。


这里重点说说数据的过滤。


在没有 Java8 之前,我们过滤数据一般是这样做的:


public List<User> filterUser(List<User> userList) {if(CollectionUtils.isEmpty(userList)) {return Collections.emptyList();}


List<User> resultList = Lists.newArrayList();for(User user: userList) {    if(user.getId() > 1000 && user.getAge() > 18) {       resultList.add(user);    }}return resultList;
复制代码


}通常需要另一个集合辅助完成这个功能。


但如果使用 Java8 的 filter 功能,代码会变得简洁很多,例如:


public List<User> filterUser(List<User> userList) {if(CollectionUtils.isEmpty(userList)) {return Collections.emptyList();}


return userList.stream().filter(user -> user.getId() > 1000 && user.getAge() > 18).collect(Collectors.toList());
复制代码


}代码简化了很多,完美。


但如果你对过滤后的数据,做修改了:


List<User> userList = queryUser();List<User> filterList = filterUser(userList);for(User user: filterList) {user.setName(user.getName() + "测试");}


for(User user: userList) {System.out.println(user.getName());}你当时可能只是想修改过滤后的数据,但实际上,你会把元素数据一同修改了。


意不意外,惊不惊喜?


其根本原因是: 过滤后的集合中,保存的是对象的引用,该引用只有一份数据。


也就是说,只要有一个地方,把该引用对象的 成员变量 的值,做修改了,其他地方也会同步修改。


如下图所示:5. 自动拆箱的坑


Java5 之后,提供了 自动装箱 和 自动拆箱 的功能。


自动装箱是指:JDK 会把基本类型,自动变成包装类型。


比如:


Integer integer = 1;等价于:


Integer integer = new Integer(1);而自动拆箱是指:JDK 会把包装类型,自动转换成基本类型。


例如:


Integer integer = new Integer(2);int sum = integer + 5;等价于:


Integer integer = new Integer(2);int sum = integer.intValue() + 5;但实际工作中,我们在使用自动拆箱时,往往忘记了判空,导致出现 NullPointerException 异常。


5.1 运算


很多时候,我们需要对传入的数据进行计算,例如:


public class Test2 {public static void main(String[] args) {System.out.println(add(new Integer(1), new Integer(2)));}


private static Integer add(Integer a, Integer b) {    return a + b;}
复制代码


}如果传入了 null 值:


System.out.println(add(null, new Integer(2)));则会直接报错。


5.2 传参


有时候,我们定义的某个方法是基本类型,但实际上传入了包装类,比如:


public static void main(String[] args) {Integer a = new Integer(1);Integer b = null;System.out.println(add(a, b));}


private static Integer add(int a, int b) {return a + b;}如果出现 add 方法报 NullPointerException 异常,你可能会懵逼,int 类型怎么会出现空指针异常呢?


其实,这个问题出在:Integer 类型的参数,其实际传入值为 null,JDK 字段拆箱,调用了它的 intValue 方法导致的问题。


  1. replace 的坑


很多时候我们在使用字符串时,想把字符串比如:ATYSDFA*Y 中的字符 A 替换成字符 B,第一个想到的可能是使用 replace 方法。


如果想把所有的 A 都替换成 B,很显然可以用 replaceAll 方法,因为非常直观,光从方法名就能猜出它的用途。


那么问题来了:replace 方法会替换所有匹配字符吗?


jdk 的官方给出了答案。


该方法会替换每一个匹配的字符串。


既然 replace 和 replaceAll 都能替换所有匹配字符,那么他们有啥区别呢?


replace 有两个 重载 的方法。


其中一个方法的参数:char oldChar 和 char newChar,支持字符的替换。


source.replace('A', 'B')另一个方法的参数是:CharSequence target 和 CharSequence replacement,支持字符串的替换。


source.replace("A", "B")而 replaceAll 方法的参数是:String regex 和 String replacement,即基于 正则表达式 的替换。


例如对普通字符串进行替换:


source.replaceAll("A", "B")使用正则表达替换(将*替换成 C):


source.replaceAll("\*", "C")顺便说一下,将 * 替换成 C 使用 replace 方法也可以实现:


source.replace("*", "C")小伙们看到看到二者的区别了没?使用 replace 方法无需对特殊字符进行转义。


不过,千万注意,切勿使用如下写法:


source.replace("\*", "C")这种写法会导致字符串无法替换。


还有个小问题,如果我只想替换第一个匹配的字符串该怎么办?


这时可以使用 replaceFirst 方法:


source.replaceFirst("A", "B")说实话,这里内容都很基础,但越基础的东西,越容易大意失荆州,更容易踩坑。


最后,统计一下,这些坑一个都没踩过的同学,麻烦举个手。

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
很不起眼的6个bug,90%的程序员就算写了10年代码也肯定都踩过!_Java_Java-fenn_InfoQ写作社区