什么时候要考虑判空呢? 最常见的就那么三种情况
使用调用某个方法得到的返回值之前,方法的api说明中明确指出可能会返回空,或者api文档不靠谱。
使用传入的参数前。
获取到一个多层嵌套对象,使用内层对象之前(链式调用尤其要小心)。
如果不做良好的判空处理,NullPointerException就会发生,有的时候会引发很致命的故障。
除了上面三种情况,再根据我的经验列举一些发生NPE的常见情况:
OR映射的时候,一些预期中不会为空的数据变成了空,框架又没有做防御性处理。
常见的低级错误:调用toString(), compare()等方法不判空(统计出来的N多故障是toString()的时候出现的空,很低级,但是事实如此)。
异步调用,结果值没有返回时就使用。(看一下Future)
调用超时处理不当。
RPC的被调用方做了修改,没通知调用方/RPC调用失败(很多原因,如鉴权废了,底层链接有问题,超时等)。
基本类型的包裹型实例如果被赋值为空,且被自动拆箱时。
syncronized了一个空对象。
那么如何较好的处理null呢?
if (obj != null){ //do something } 复制代码
if(obj == null){ //错误处理,一般是返回约定的错误,或者抛Exception } 复制代码
@Test public void testGuavaNotNull(){ Object obj = null; String errorMessage = "obj is null"; Preconditions.checkNotNull(obj,errorMessage); } //一般的使用方式是这样的 对于一个输入参数或者调用其它方法返回的值 objToBeChecked try{ ... // 异常消息收集或构造 obj = Preconditions.checkNotNull(objToBeChecked,errorMessage); ... }catch(NullPointerException npe){ // 异常处理 } 复制代码
Java基础库的框架中其实也提供了简单的静态方法 java.util.Objects.reqireNonNull(T obj),但是Guava框架的好处是,你可以构造具体的errorMessage传递给检测函数,从而在异常被抛出后,程序员可以得到更具体的异常信息。
空处理常见的工具类还有Spring的ObjectUtils,Apache Common Lang的ObjectUtils (这个工具类其实非常强大,里边函数的处理Null的思路也非常值得借鉴)等,举一个例子。假设一个场景,需要多对象的判空逻辑,就可以使用工具类的线程函数,让语义更清晰,减少错误。
//比较冗长 if(obj1 == null || obj2 == null || boj3 == null){ //do something } //ObjectUtils 的方式:语义直接,不易出错 (if(ObjectUtils.anyNotNull(obj1,obj2,obj3))){ // do something } 复制代码
也有框架提供了类似于Assert的 工具类,如Lombok 的@NonNull注解,如果被注解的对象是空值,直接会抛出NPE,用作对输入参数的检查,会让代码变优雅不少,**语义也更清晰**。类似这样的工具遵循了JSR305, 具体实现有很多,比如findbugs,SpotBugs,Spring,AndroidTookit等都提供了这样的注解。注解可以非常方便的挂在方法、输入参数上。
//Lombok的例子,如果 obj为null,直接抛出NPE异常, public void LombokNullCheck(@NonNull Object obj){ // 可以直接使用obj } 复制代码
class Passenger{ private Seat seat; private Cert cert; Cert getCert(){ return cert; } ... } class Cert{ private PersonalInfo pi; PersonalInfo getPersonalInfo(){ return pi; } } class PersonalInfo{ private String name; String getName();{ return name; } } 复制代码
对于嵌套比较深的类,下面这样的代码太常见了,大段的&&条件判断非常容易出错,**代码可读性也非常差。**
Passenger passenger = SomeMehtod.getPassenger(); if(passenger != null && passenger.getCert() != null && passenger.getCert().getPersonalInfo != null){ return passenger.getCert().getPersonalInfo().getName(); } else return "default name"; //有更差的实践是写成下面的多重嵌套if模式,这样在真实情况下很容易缩进七八层,甚至十几层,代码可读性基本上就没了。 if(passenger != null){ if (passenger.getCert() != null){ if(passenger.getCert().getPersonalInfo() != null){ return passenger.getCert().getPersonalInfo().getName(); }else return "default name" } } //还有更差的实践,比如生成很多只用一次的中间对象。对了,就是把 Cert,PersonalInfo 再都new出来。代码太难看,就不补全了。 复制代码
使用Optional 配合lambda表达式的效果,见下面代码,是不是非常简洁清晰了?
Passenger passenger = SomeMehtod.getPassenger(); return Optional.ofNullable(passenger) .map(Passenger::getCert) .map(Cert::getPersonalInfo) .map(PersonalInfo::getName) .orElse("default name"); 复制代码
如果,需要抛出一个空指针异常而不是返回默认值,可以写成下面这样。
Passenger passenger = SomeMehtod.getPassenger(); return Optional.ofNullable(passenger) .map(Passenger::getCert) .map(Cert::getPersonalInfo) .map(PersonalInfo::getName) .orElesThrow(NullPointerException::new) 复制代码
对于嵌套类的空判断,使用Optional比传统的层层剥皮判断要好很多 。其他新一些的语言如Swift,Kotlin都提供了内建语言支持,会更加优雅,代码量也会少很多。需要特别说明的是,最好通读Java8的Stream API文档,了解兰布达表达式的正确使用方式,才能以正确的方式打开。如果不结合Stream API来看,Optional反而让代码变得更冗余了。
作为调用方的义务
尽量不要把null当做一个参数传递。这个其实很好了解,当你传入一个null的时候,如果不知道被调方法的具体实现,你不知道会触发什么。假如被调用函数没做空处理,假如这个null又被传递了出去,影响就不可控了,除非你知道被调用方法的所有具体实现。
作为API提供方的义务
对传入的参数做判空处理;良好的API文档标明传入空值的后果和什么情况下会抛出NPE或者包装了的其它异常。使用@NonNull 这种assert工具;不要继续传递传入的空值。抛出NPE比返回一个null要好的多(如果考虑性能影响则另当别论),尽量不返回null,如果必须要,文档一定要说明。
单元测试的防护网很可能救你一命。一些代码的生命周期很长。有些地方的判空处理如果有对应的单元测试覆盖这部分逻辑,当其他维护者(非常有可能是其他维护者)不小心修改了这部分逻辑,对应的单元测试很可能会救系统一命。它会提醒新来的维护者:你踩了个地雷,好好看看是不是应该这样。我见过好几次这样的救命案例了。
NPE一定要严防死守么? 答案是否定的。识别异常的含义,并正确利用,是一个程序员的素养。
一个复杂嵌套对象中,我们要对所有的字段判空么?那岂不是要写死人了。 文章最初的那个例子就是这样:如果对所有字段全写判空,代码量会很感人,读起来会更感人。其实这个问题也是有解的,如果你只关注部分字段,就只对它们判空并读取,别的字段别碰。如果必须要碰,在外层做catch(会影响性能,别在性能关键点做)。另外,读取的数据源其实应该有很好的注释,并应该有nullable 的assert,修改数据的人应该仔细读这些注释,并不去破坏规则,毕竟软件是多人协作。大家都遵守约定,才能降低协作中产生的错误概率。
再聊两句防御性编程 : 防御性编程上世纪80年代就提出来了,其核心观念是:“预防你认为不可能发生的,时间长了,它一定会发生。”,防御性编程里有好多套路:永远不要相信用户输入,调用时永远做异常判断等等等等。有一些防御性编程的意识是一件非常好的事儿,能防止很多低级错误产生。但是一些不必要的严防死守,会让代码变得很丑陋和复杂。很多事儿,过犹不及。