前一段时间,项目紧急迭代,临时加入了一个新功能,具体功能就不描述了,反正就是业务功能:用户通过浏览器在系统界面上操作,然后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
,可参考我的前文《Lombok,嗯,真香香香香香香!》【点击可跳转】
手动进行 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 userList = userMapper.queryUserList( userType );if( userList != null ) {//此处免不了对userList进行判空 for( User user : userList ) { // ... // 对user对象进行操作 // ... }}
如果用 Optional
接口进行改造,可以写为:
List 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
接口对函数的返回值进行了包裹:
OptionalgetScore( Long studentId ) { // ...}
这样当调用者调用这个函数时,他可以清清楚楚地看到 getScore()
这个函数的返回值的特殊性(有可能为 null
),这样一个警示一定会很大几率上帮助调用者规避 null
指针异常。
上面所述的 Optional
语法只是在 JDK 1.8
版本后才开始引入,那还在用 JDK 1.8
版本之前的老项目怎么办呢?
没关系!
Google
大名鼎鼎的 Guava
库中早就提供了 Optional
接口来帮助优雅地处理 null
对象问题,其本质也是在可能为 null
的对象上做了一层封装,使用起来和JDK本身提供的 Optional
接口没有太大区别。
你只需要在你的项目里引入 Google
的 Guava
库:
com.google.guava
guava
即可享受到和 Java8
版本开始提供的 Optional
一样的待遇!
每天进步一点点!Peace!
2020.01.14晚
给个[在看],是对程序羊最大的支持