编程时以什么方式对待java的空指针问题

回主页

好几年前我已转入使用kotlin语言开发了。在kotlin中,有一个可空类型与不可空类型的区分,它有效避免了线上的NPE(NullPointerException)问题。但java语言应用广泛,目前依然大面积应用在android app开发与web后端开发中。现在再转回到java编程中,一时觉得java比kotlin繁琐很多。java在对待空指针null依然有点困难。如果项目大了,一个方法体上百行,方法体内部可能会出现处处判空。

举一例子,如果要写健壮的代码,需要层层判空。

class Driver {
    private Car car;
    private Phone phone;

    public void setCar(Car car) {
        this.car = car;
    }
    
    public void setPhone(Phone phone) {
        this.phone = phone;
    }

    public void operate() {
        if (phone != null) {
            phone.call();
        }
        
        if (car != null) {
            if (car.getEngine() != null) {
                car.getEngine().run();
            }
        }
    }
}

这样写代码带来的感受是在冰上走路,往前探一步走一步,太小心谨慎。于是在想,有没有方法对待java的空指针。

第一个方法,使用注解,把空指针判断向代码的上游推。

使用jakarta.annotation.Nonnull与jakarta.annotation.Nullable。举例说明。

class Driver {
    @Nonnull
    private Car car;
    @Nonnull
    private Phone phone;

    public void setCar(@Nonnull Car car) {
        this.car = car;
    }

    public void setPhone(@Nonnull Phone phone) {
        this.phone = phone;
    }

    public void operate() {
        // 这样顺畅多了
        phone.call();
        car.getEngine().run();
    }
}

class Car {
    @Nonnull
    private Engine engine;

    public void setEngine(@Nonnull Engine engine) {
        this.engine = engine;
    }

    public @Nonnull Engine getEngine() {
        return engine;
    }
}

把空指针判断向代码的上游推。是说,如果遇到了需要空指针判断,则把这个任务推向调用者,直到不能再往上推为止。举例说明如下,把空指针判断的任务向调用者Controller推,由Controller做这件事情。如果还有Controller的调用者,那么由该调用者来做空指针判断。

class Controller {
    public void method(Driver driver,
                       Car car,
                       Engine engine,
                       Phone phone) {
        if (driver == null) {
            return;
        }

        if (car != null && engine != null && phone != null) {
            car.setEngine(engine);
            driver.setCar(car);
            driver.setPhone(phone);
            driver.operate();
        }
    }
}

第二个方法,在项目里集成checker framework。

checker framework是由MIT一实验室发明的,后来由华盛顿大学一实验室继续维护。它的官网是https://checkerframework.org/。它定义了一堆注解(其中也有@Nullable与@NonNull)。在初始化、给变量赋值、返回值等地方,它提供了编译期检查机制。如果代码可能有空指针操作失误的地方,在build项目时它就会抛出异常,中断build过程。

但是,它过于严谨,在纯java编程中还行。如果应用到大型项目里比如spring web应用里,它在处理spring、mybatis等依赖注入与反射时,由于过于严谨,使得编程非常不方便。我在web项目里试过一段时间,还是放弃使用它了。觉得checker framework适合纯java的实验性质的编程项目。

第三个方法,在项目里集成jspecify与nullaway。

jspecify最初由谷歌公司发起,逐渐变成由多个巨头公司联合推出的一个规范。它统一了不同公司或组织的Nullable、Nonnull、NonNull注解。据说spring以后也要使用它了。它的官网是https://jspecify.dev/。jspecify有4个主要注解:NonNull、Nullable、NullMarked、NullUnmarked。在代码中使用这些注解,可以按照规范去声明对空指针的操作。这里不去展开讨论它怎么使用。

jspecify是一规范,nullaway就是规范的执行者。当build项目时,nullaway会检查项目中受jspecify的注解所影响的代码处,找出对空指针的不当操作,并抛出error,中断build过程。nullaway的对应的网站是http://t.uber.com/nullaway。举一例子。

public class Driver {
    @NonNull
    private Car car;
    @NonNull
    private Phone phone;

    public Driver(@Nullable Car car, @NonNull Phone phone) {
        this.car = car;
        this.phone = phone;
    }

    public void operate() {
        phone.call();
        car.getEngine().run();
    }
}

在Driver的构造方法中,把可空的参数car赋给不可空的私有字段car。build项目时nullaway会报出如下内容。

java: [NullAway] initializer method does not guarantee @NonNull field car (line 30) is initialized along all control-flow paths  (remember to check for exceptions or early returns).
     (see http://t.uber.com/nullaway )

目前想到是这3种方法,我现在正在试第3种方法。是否还有别的好方法,也请大家留言提出。

回主页

评论: