前言
对于编程开发者而言空指针异常是非常常见的,基本上各类编程语言都存在空指针异常,对于Java开发者而言,相信NullPointerException是大家再熟悉不过的。虽然空指针很常见,但是空指针对系统造成的危害却是不容忽视的,因此很多现代编程语言在语法上就对空指针进行了很多避免,比如Kotlin。但是对于Java语言有没有什么好的方式呢?
空指针分析
对于空指针的出现,其实一般可以归纳为以下几个原因:
- 对于方法入参没有严格校验
- 对于方法返回值没有严格的校验
但是更本质的原因应该是对于调用的其他API没有充分的了解,使用时不知道API方法的入参是否可以接受null,不确定方法的返回值是否为null。
在开发项目的时候相信很多人对于使用的API和方法都会习惯地去看一看方法对应的代码看看是否可以接受null,是否可以返回null值,虽然这样可以有效避免空指针,但是这种方式会降低工作效率,没法得到大家共识,更没办法办法在部门中进行推广。所以需要更行之有效的方式。
空指针避免方法
避免空指针的的目标不仅在于有效避免和减少空指针,更在与增加方法可读性,使调用者明确知道方法的入参是否可以接受null,方法的返回值是否可以返回null。
1.返回值空指针避免
在Java8之前相信很多人都使用Google Guava的Optional,能够有效避免返回值造成的空指针,因此后来也就在Java8中引入了Optional。但是仅仅靠Optional是不够的。方法的返回类型不是Optional不能说明方法是否会返回null,因此需要一种更明确的方式。而我个人比较推荐的方式就是标注注解,目前很多开源项目也是采用的这种方式。这样不仅能有效避免空指针,还能增加方法的可读性。
2.方法入参控制值避免
方法入参是否可以接受null,这里除了标注注解没有其他更好的方式。标注注解同样能提升API的可读性。
Optional的使用
Optional的选择和使用比较简单,在Java8之前使用Guava Optional,在Java8之后使用语言本身自带的Optional。需要注意的是Optional并不适用于方法入参。
示例:
// Java8
public static Optional<String> valueOf(Integer number) {
if (number == null) {
return Optional.empty();
}
String str = String.valueOf(number);
return Optional.of(number);
}
注解的使用
对于注解的选择就比较困难了,因为可选择的注解太多了,而且需要考虑的因素也比较多。
1.常用的注解:
- findbugs
- edu.umd.cs.findbugs.annotations.NonNull
- edu.umd.cs.findbugs.annotations.Nullable
- jsr305
- javax.annotation.Nonnull
- javax.annotation.Nullable
- spring-core
- org.springframework.lang.NonNull
- org.springframework.lang.Nullable
- javax-validator
- javax.validation.constraints.NotNull
- javax.validation.constraints.Null
- android-support
- android.support.annotation.NonNull
- android.support.annotation.Nullable
- eclipse-jdt
- org.eclipse.jdt.annotation.NonNull
- org.eclipse.jdt.annotation.Nullable
- jetbrains-annotations
- org.jetbrains.annotations.NotNull
- org.jetbrains.annotations.Nullable
- lombok
- lombok.NonNull
- rt.jar
- com.sun.istack.internal.NotNull
- com.sun.istack.internal.Nullable
2.选择因素:
- 注解完备性
- 必须同时支持null注解与非null注解,如果只支持其中一个那么使用场景将会受到很大限制
- ide代码检查
- 在ide中运行的时候如果能够对标注非null的参数和返回值进行校验,那么么将在很大程度上避免空指针
- 校验逻辑大致如下:
public static void display(@Nonnull String str) { if (str == null) { throw new IllegalArgumentException(); } // do something }
- ide注解生成
- 继承父类的方法,是否可以直接继承标注在方法参数和返回类型上的注解,这个特性是很重要的,因为在大型软件系统的中都是采用分层架构,层与层之间进行调用都是通过接口,因此不支持这个特性将会导致开发人员手动在子类方法入参和返回类型上标注注解,这无疑会大大增加工作量。
- ide智能提示
- 会对潜在产生空指针的变量进行高亮显示
- findbugs支持
- 一般的公司都会要求开发人员在ide上安装findbugs,用以扫描代码分析潜在的bug
- sonar支持
- 大型公司都会对代码进行静态分析,一般使用SonarCube,SonarCube可以继承fingbugs和pmd的校验规则,因此支持fingdbugs可以在一定程度上说明也支持Sonar
3.各类注解支持情况
注解支持库 | 空注解 | 非空注解 | findbugs支持 | sonar支持 | ide运行时检查 | ide智能提示 | ide代码生成 |
---|---|---|---|---|---|---|---|
findbugs | @NonNull | @Nullable | 支持 | 支持 | 支持 | 支持 | 支持 |
jsr305 | @Nonnull | @Nullable | 支持 | 支持 | 支持 | 支持 | 支持 |
spring-core | @NonNull | @Nullable | 不支持 | 不支持 | 不支持 | 支持 | 不支持 |
javax-validator | @NotNull | @Null | 不支持 | 不支持 | 不支持 | 不支持 | 支持 |
android-support | @NonNull | @Nullable | 不支持 | 不支持 | 支持 | 支持 | 支持 |
eclipse-jdt | @NonNull | @Nullable | 不支持 | 不支持 | 不支持 | 支持 | 支持 |
jetbrains-annotations | @NotNull | @Nullable | 不支持 | 不支持 | 支持 | 支持 | 支持 |
lombok | @NonNull | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
rt.jar | @NotNull | @Nullable | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
注意
- 测试使用的ide是Idea,Eclipse存在一定的差异
- rt.jar @NotNull @Nullable属于sun的内部包,不要使用,如果有代码检查则不会被允许通过
- eclipse-jdt和jetbrains-annotations和对应的ide绑定比较紧密不要轻易使用
- javax-validator和lombok主要是运行时的参数检查
- fingbugs原生的注解已经不再推荐,推荐使用jsr305的注解
4.方案选择
通过以上对比,可以很好的选择出应该使用的注解是jsr305的@Nullable和@Nonnull。在使用过程中是否意味着所有方法的入参和返回值都应该标注呢?显然不是的,基本类型的入参和返回值是不需要标注@Nonnull和@Nullable注解的;private方法,package方法,protected方法也是不需要标注的;其实归纳为一句话,就是public方法上的非基本类型参数和返回值需要标注。那么Optional返回类型是否需要标注呢,这里没有一个明确的定论,个人认为觉得标上比较好。
标注注解只是一种声明声明,没有强制的约束,因此标注@NotNull的入参应该增加参数校验才合理,这也是一般开源项目的普遍做法。
-
maven依赖
<dependency> <groupId>com.google.code.findbugs</groupId> <artifactId>jsr305</artifactId> <version>3.0.2</version> </dependency>
-
使用示例
// 非空注解 @Nonnull public Integer add(@Nonnull Integer number1, @Nonnull Integer number2) { Assert.notNull(number1, "number1 must not be null"); Assert.notNull(number2, "number2 must not be null"); return numnber1 + number2; } // 空注解 public static boolean isBlank(@Nullable String str) { return str == null || str.trim().length() == 0; } // Optional @Nonnull public static Optional<Integer> parseInte(@Nullable String str) { if (str == null) { return Optional.empty(); } return Optional.of(Integer.parseInt(str)); }
Idea jsr305注解配置
配置之后,idea可以对空指针进行校验,也能进行智能提示。
结语
这虽然只是一个很小的改进,但是会极大程度提升代码的质量,大幅度降低系统中的空指针异常的数量,提升系统的健壮性。