代码炸了
前一段时间,项目紧急迭代,临时加入了一个新功能:用户通过浏览器在系统界面上操作,然后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";
}
手动空指针保护
手动进行 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
判空实在有点心累。
消除俄罗斯套娃式判空
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
库:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
即可享受到和 Java8
版本开始提供的 Optional
一样的待遇!