2020-02-26 17:31:17

只因少写一个判空,我的代码上线后炸了!

12 / 0 / 0 / 0

作者: 玉刚说 来源: 玉刚说

 代码炸了!

前一段时间,项目紧急迭代,临时加入了一个新功能,具体功能就不描述了,反正就是业务功能:用户通过浏览器在系统界面上操作,然后Java后台代码做一些数据的查询、计算和整合的工作,并对第三方提供了操作接口。

当晚凌晨上线,本系统内测试,完美通过!

第二天将接口对外提供,供第三方系统调用,duang !工单立马来了

很明显,后台代码炸了!拉了一下后台日志,原来又是烦人的空指针异常 NullPointerException !

为此,本文痛定思痛,关于 null 空指针异常问题的预防和解决,详细整理成文,并严格反思:我们到底在代码中应该如何防止空指针异常所导致的Bug?

 最常见的输入判空  

对输入判空非常有必要,并且常见,举个栗子:

public String addStudent( Student student ) {
   // ...
}

无论如何,你在进行函数内部业务代码编写之前一定会对传入的 student 对象本身以及每个字段进行判空或校验:

public String addStudent( Student student ) {

    if( student == null )
        return "传入的Student对象为null,请传值";

    if( student.getName()==null || "".equals(student.getName()) )
        return "传入的学生姓名为空,请传值";
    if( student.getScore()==null )
        return "传入的学生成绩为null,请传值";
    if( (student.getScore()<0) || (student.getScore()>100) )
        return "传入的学生成绩有误,分数应该在0~100之间";
    if( student.getMobile()==null || "".equals(student.getMobile()) )
        return "传入的学生电话号码为空,请传值";
    if( student.getMobile().length()!=11 )
        return "传入的学生电话号码长度有误,应为11位";

    studentService.addStudent( student ); // 将student对象存入MySQL数据库
    return "SUCCESS";
}

为了避免人肉手写 这种繁杂的输入判空,我们最起码可以用两种方式来进行优雅的规避

方法一: 借助Spring框架本身的注解  @NotNull

方法二: 借助Lombok工具的注解 @NonNull

 手动空指针保护  

手动进行 if(obj !=null) 的判空自然是最全能的,也是最可靠的,但是怕就怕俄罗斯套娃式 的 if 判空。

举例一种情况:

为了获取:省(Province)→市(Ctiy)→区(District)→街道(Street)→道路名(Name)

作为一个“严谨且良心” 的后端开发工程师,如果手动地进行空指针保护,我们难免会这样写:

public String getStreetName( Province province ) {
    if( province != null ) {
        City city = province.getCity();
        if( city != null ) {
            District district = city.getDistrict();
            if( district != null ) {
                Street street = district.getStreet();
                if( street != null ) {
                    return street.getName();
                }
            }
        }
    }
    return "未找到该道路名";
}

为了获取到链条最终端的目的值,直接链式取值 必定有问题,因为中间只要某一个环节的对象为 null ,则代码一定会炸,并且抛出 NullPointerException 异常,然而俄罗斯套娃式的 if 判空实在有点心累。

 消除俄罗斯套娃式判空  

在我的前文《以后要是再写for循环,我就捶自己》 里已经提及过,我们也可以利用Java的函数式编程接口 Optional 来进行优雅的判空!

Optional 接口本质是个容器,你可以将你可能为 null 的变量交由它进行托管,这样我们就不用显式对原变量进行 null 值检测,防止出现各种空指针异常。

Optional语法专治上面的俄罗斯套娃式 if 判空 ,因此上面的代码可以重构如下:

public String getStreetName( Province province ) {
    return Optional.ofNullable( province )
            .map( i -> i.getCity() )
            .map( i -> i.getDistrict() )
            .map( i -> i.getStreet() )
            .map( i -> i.getName() )
            .orElse( "未找到该道路名" );
}

漂亮!嵌套的 if/else 判空灰飞烟灭!

解释一下执行过程:

  • ofNullable(province )  :它以一种智能包装的方式来构造一个 Optional 实例, province 是否为 null 均可以。如果为 null ,返回一个单例空 Optional 对象;如果非 null ,则返回一个 Optional 包装对象
  • map(xxx ) :该函数主要做值的转换,如果上一步的值非 null ,则调用括号里的具体方法进行值的转化;反之则直接返回上一步中的单例 Optional 包装对象
  • orElse(xxx ) :很好理解,在上面某一个步骤的值转换终止时进行调用,给出一个最终的默认值

当然实际代码中倒很少有这种极端情况,不过普通的 if(obj !=null) 判空也可以用 Optional 语法进行改写,比如很常见的一种代码:

List<User> userList = userMapper.queryUserList( userType );
if( userList != null ) {//此处免不了对userList进行判空
  for( User user : userList ) {
    // ...
    // 对user对象进行操作
    // ...
  }
}

如果用 Optional 接口进行改造,可以写为:

List<User> userList = userMapper.queryUserList( userType );
Optional.ofNullable( userList ).ifPresent(
  list -> {
    for( User user : list ) {
      // ...
      // 对user对象进行操作
      // ...
    }
  }
)

这里的 ifPresent() 的含义很明显:仅在前面的 userList 值不为 null 时,才做下面其余的操作。

 只是一颗语法糖  

没有用过 Optional 语法的小伙伴们肯定感觉上面的写法非常甜蜜 !然而褪去华丽的外衣,甜蜜的 Optional 语法底层依然是朴素的语言级写法,比如我们看一下 Optional 的 ifPresent() 函数源码,就是普通的 if 判断而已:

那就有人问:我们何必多此一举 ,做这样一件无聊的事情呢?

其实不然!

用 Optional 来包装一个可能为 null 值的变量,其最大意义其实仅仅在于给了调用者一个明确的警示

怎么理解呢?

比如你写了一个函数,输入学生学号 studentId ,给出学生的得分 :

Score getScore( Long studentId ) {
  // ...
}

调用者在调用你的方法时,一旦忘记 if(score !=null) 判空,那么他的代码肯定是有一定 bug 几率的。

但如果你用 Optional 接口对函数的返回值进行了包裹:

Optional<Score> getScore( Long studentId ) {
  // ...
}

这样当调用者调用这个函数时,他可以清清楚楚地看到 getScore() 这个函数的返回值的特殊性(有可能为 null ),这样一个警示一定会很大几率上帮助调用者规避 null 指针异常。

 老项目该怎么办?  

上面所述的 Optional 语法只是在 JDK 1.8 版本后才开始引入,那还在用 JDK 1.8 版本之前的老项目怎么办呢?

没关系!

Google 大名鼎鼎的 Guava 库中早就提供了 Optional 接口来帮助优雅地处理 null 对象问题,其本质也是在可能为 null 的对象上做了一层封装,使用起来和JDK本身提供的 Optional 接口没有太大区别。

你只需要在你的项目里引入 Google 的 Guava 库:

com.google.guavaguava `

PS: 如本文对您有疑惑,可加QQ:1752338621 进行讨论。

0 条评论

0
0
官方
微信
官方微信
Q Q
咨询
意见
反馈
返回
顶部