转载

Java null最佳实践

好的习惯

什么时候要考虑判空呢? 最常见的就那么三种情况

  • 使用调用某个方法得到的返回值之前,方法的api说明中明确指出可能会返回空,或者api文档不靠谱。

  • 使用传入的参数前。

  • 获取到一个多层嵌套对象,使用内层对象之前(链式调用尤其要小心)。

如果不做良好的判空处理,NullPointerException就会发生,有的时候会引发很致命的故障。

除了上面三种情况,再根据我的经验列举一些发生NPE的常见情况:

  • OR映射的时候,一些预期中不会为空的数据变成了空,框架又没有做防御性处理。

  • 常见的低级错误:调用toString(), compare()等方法不判空(统计出来的N多故障是toString()的时候出现的空,很低级,但是事实如此)。

  • 异步调用,结果值没有返回时就使用。(看一下Future)

  • 调用超时处理不当。

  • RPC的被调用方做了修改,没通知调用方/RPC调用失败(很多原因,如鉴权废了,底层链接有问题,超时等)。

  • 基本类型的包裹型实例如果被赋值为空,且被自动拆箱时。

  • syncronized了一个空对象。

那么如何较好的处理null呢?

  • 使用前判空 。这是最常见的使用办法。
if (obj != null){
    //do something
}
复制代码
  • 较好的编程方式是提前判断错误(可以参考Guard Clause 模式),这样能够消除过度嵌套的情况出现。
if(obj == null){
    //错误处理,一般是返回约定的错误,或者抛Exception
}
复制代码
  • 也**可以使用一些工具类减少代码量,让编程模式更清晰**。例如google的Guava框架提供了Preconditions工具,来帮助程序员快速的做参数检测,Preconditions里有一个静态方法checkNotNull,如果不为空,则返回被检测的对象本身,如果被检测的对象为空,则会抛出NullPointerException。
@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
}
复制代码
  • Java8提供的改进 ,Java8提供了一个叫做Optional的类型,在实战中非常实用,Optional类型和stream API一并使用的话,能让空检查变得更加优雅,特别是复杂嵌套对象的空检查。但读很多人的代码,发现他们并没有习惯这样使用。先上一段代码感受一下。
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年代就提出来了,其核心观念是:“预防你认为不可能发生的,时间长了,它一定会发生。”,防御性编程里有好多套路:永远不要相信用户输入,调用时永远做异常判断等等等等。有一些防御性编程的意识是一件非常好的事儿,能防止很多低级错误产生。但是一些不必要的严防死守,会让代码变得很丑陋和复杂。很多事儿,过犹不及。

原文  https://juejin.im/post/5c4fb52ef265da615f77a1f3
正文到此结束
Loading...