好几年前我已转入使用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种方法。是否还有别的好方法,也请大家留言提出。