Java 中任何对象都有可能为空,当我们调用空对象的方法时就会抛出 NullPointerException
空指针异常,这是一种非常常见的错误类型。我们可以使用若干种方法来避免产生这类异常,使得我们的代码更为健壮。本文将列举这些解决方案,包括传统的空值检测、编程规范、以及使用现代 Java 语言引入的各类工具来作为辅助。
运行时检测
最显而易见的方法就是使用 if (obj == null)
来对所有需要用到的对象来进行检测,包括函数参数、返回值、以及类实例的成员变量。当你检测到 null
值时,可以选择抛出更具针对性的异常类型,如 IllegalArgumentException
,并添加消息内容。我们可以使用一些库函数来简化代码,如 Java 7 开始提供的 Objects#requireNonNull
方法:
1 | public void testObjects(Object arg) { |
Guava 的 Preconditions
类中也提供了一系列用于检测参数合法性的工具函数,其中就包含空值检测:
1 | public void testGuava(Object arg) { |
我们还可以使用 Lombok 来生成空值检测代码,并抛出带有提示信息的空指针异常:
1 | public void testLombok( { Object arg) |
生成的代码如下:
1 | public void testLombokGenerated(Object arg) { |
这个注解还可以用在类实例的成员变量上,所有的赋值操作会自动进行空值检测。
编程规范
通过遵守某些编程规范,也可以从一定程度上减少空指针异常的发生。
- 使用那些已经对
null
值做过判断的方法,如String#equals
、String#valueOf
、以及三方库中用来判断字符串和集合是否为空的函数:
1 | if (str != null && str.equals("text")) {} |
- 如果函数的某个参数可以接收
null
值,考虑改写成两个函数,使用不同的函数签名,这样就可以强制要求每个参数都不为空了:
1 | public void methodA(Object arg1) { |
- 如果函数的返回值是集合类型,当结果为空时,不要返回
null
值,而是返回一个空的集合;如果返回值类型是对象,则可以选择抛出异常。Spring JdbcTemplate 正是使用了这种处理方式:
1 | // 当查询结果为空时,返回 new ArrayList<>() |
静态代码分析
Java 语言有许多静态代码分析工具,如 Eclipse IDE、SpotBugs、Checker Framework 等,它们可以帮助程序员检测出编译期的错误。结合 @Nullable
和 @Nonnull
等注解,我们就可以在程序运行之前发现可能抛出空指针异常的代码。
但是,空值检测注解还没有得到标准化。虽然 2006 年 9 月社区提出了 JSR 305 规范,但它长期处于搁置状态。很多第三方库提供了类似的注解,且得到了不同工具的支持,其中使用较多的有:
javax.annotation.Nonnull
:由 JSR 305 提出,其参考实现为com.google.code.findbugs.jsr305
;org.eclipse.jdt.annotation.NonNull
:Eclipse IDE 原生支持的空值检测注解;edu.umd.cs.findbugs.annotations.NonNull
:SpotBugs 使用的注解,基于findbugs.jsr305
;org.springframework.lang.NonNull
:Spring Framework 5.0 开始提供;org.checkerframework.checker.nullness.qual.NonNull
:Checker Framework 使用;android.support.annotation.NonNull
:集成在安卓开发工具中;
我建议使用一种跨 IDE 的解决方案,如 SpotBugs 或 Checker Framework,它们都能和 Maven 结合得很好。
SpotBugs 与 @NonNull
、@CheckForNull
SpotBugs 是 FindBugs 的后继者。通过在方法的参数和返回值上添加 @NonNull
和 @CheckForNull
注解,SpotBugs 可以帮助我们进行编译期的空值检测。需要注意的是,SpotBugs 不支持 @Nullable
注解,必须用 @CheckForNull
代替。如官方文档中所说,仅当需要覆盖 @ParametersAreNonnullByDefault
时才会用到 @Nullable
。
官方文档 中说明了如何将 SpotBugs 应用到 Maven 和 Eclipse 中去。我们还需要将 spotbugs-annotations
加入到项目依赖中,以便使用对应的注解。
1 | <dependency> |
以下是对不同使用场景的说明:
1 |
|
对于 Eclipse 用户,还可以使用 IDE 内置的空值检测工具,只需将默认的注解 org.eclipse.jdt.annotation.Nullable
替换为 SpotBugs 的注解即可:
Checker Framework 与 @NonNull
、@Nullable
Checker Framework 能够作为 javac
编译器的插件运行,对代码中的数据类型进行检测,预防各类问题。我们可以参照 官方文档,将 Checker Framework 与 maven-compiler-plugin
结合,之后每次执行 mvn compile
时就会进行检查。Checker Framework 的空值检测程序支持几乎所有的注解,包括 JSR 305、Eclipse、甚至 lombok.NonNull
。
1 | import org.checkerframework.checker.nullness.qual.Nullable; |
Checker Framework 默认会将 @NonNull
应用到所有的函数参数和返回值上,因此,即使不添加这个注解,以下程序也是无法编译通过的:
1 | private Object returnNonNull() { |
Checker Framework 对使用 Spring Framework 5.0 以上的用户非常有用,因为 Spring 提供了内置的空值检测注解,且能够被 Checker Framework 支持。一方面我们无需再引入额外的 Jar 包,更重要的是 Spring Framework 代码本身就使用了这些注解,这样我们在调用它的 API 时就能有效地处理空值了。举例来说,StringUtils
类里可以传入空值的函数、以及会返回空值的函数都添加了 @Nullable
注解,而未添加的方法则继承了整个框架的 @NonNull
注解,因此,下列代码中的空指针异常就可以被 Checker Framework 检测到了:
1 | // 这是 spring-core 中定义的类和方法 |
Optional
类型
Java 8 引入了 Optional<T>
类型,我们可以用它来对函数的返回值进行包装。这种方式的优点是可以明确定义该方法是有可能返回空值的,因此调用方必须做好相应处理,这样也就不会引发空指针异常。但是,也不可避免地需要编写更多代码,而且会产生很多垃圾对象,增加 GC 的压力,因此在使用时需要酌情考虑。
1 | Optional<String> opt; |
方法的链式调用很容易引发空指针异常,但如果返回值都用 Optional
包装起来,就可以用 flatMap
方法来实现安全的链式调用了:
1 | String zipCode = getUser() |
Java 8 Stream API 同样使用了 Optional
作为返回类型:
1 | stringList.stream().findFirst().orElse("default"); |
此外,Java 8 还针对基础类型提供了单独的 Optional
类,如 OptionalInt
、OptionalDouble
等,在性能要求比较高的场景下很适用。
其它 JVM 语言中的空指针异常
Scala 语言中的 Option
类可以对标 Java 8 的 Optional
。它有两个子类型,Some
表示有值,None
表示空。
1 | val opt: Option[String] = Some("text") |
除了使用 Option#isEmpty
判断,还可以使用 Scala 的模式匹配:
1 | opt match { |
Scala 的集合处理函数库非常强大,Option
则可直接作为集合进行操作,如 filer
、map
、以及列表解析(for-comprehension):
1 | opt.map(_.trim).filter(_.length > 0).map(_.toUpperCase).getOrElse("DEFAULT") |
Kotlin 使用了另一种方式,用户在定义变量时就需要明确区分 可空和不可空类型。当可空类型被使用时,就必须进行空值检测。
1 | var a: String = "text" |
Kotlin 的特性之一是与 Java 的可互操作性,但 Kotlin 编译器无法知晓 Java 类型是否为空,这就需要在 Java 代码中使用注解了,而 Kotlin 支持的 注解 也非常广泛。Spring Framework 5.0 起原生支持 Kotlin,其空值检测也是通过注解进行的,使得 Kotlin 可以安全地调用 Spring Framework 的所有 API。
结论
在以上这些方案中,我比较推荐使用注解来预防空指针异常,因为这种方式十分有效,对代码的侵入性也较小。所有的公共 API 都应该使用 @Nullable
和 @NonNull
进行注解,这样就能强制调用方对空指针异常进行预防,让我们的程序更为健壮。
参考资料
- https://howtodoinjava.com/java/exception-handling/how-to-effectively-handle-nullpointerexception-in-java/
- http://jmri.sourceforge.net/help/en/html/doc/Technical/SpotBugs.shtml
- https://dzone.com/articles/features-to-avoid-null-reference-exceptions-java-a
- https://medium.com/@fatihcoskun/kotlin-nullable-types-vs-java-optional-988c50853692