Java中空指针异常的几种解决方案

 更新时间:2023年01月06日 10:55:29   作者:张吉Jerry  
这篇文章主要介绍了Java中空指针异常的几种解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

Java 中任何对象都有可能为空,当我们调用空对象的方法时就会抛出 NullPointerException 空指针异常,这是一种非常常见的错误类型。

我们可以使用若干种方法来避免产生这类异常,使得我们的代码更为健壮。

本文将列举这些解决方案,包括传统的空值检测、编程规范、以及使用现代 Java 语言引入的各类工具来作为辅助。

运行时检测

最显而易见的方法就是使用 if (obj == null) 来对所有需要用到的对象来进行检测,包括函数参数、返回值、以及类实例的成员变量。

当你检测到 null 值时,可以选择抛出更具针对性的异常类型,如 IllegalArgumentException,并添加消息内容。

我们可以使用一些库函数来简化代码,如 Java 7 开始提供的 Objects#requireNonNull 方法:

public void testObjects(Object arg) {
  Object checked = Objects.requireNonNull(arg, "arg must not be null");
  checked.toString();
}

Guava 的 Preconditions 类中也提供了一系列用于检测参数合法性的工具函数,其中就包含空值检测:

public void testGuava(Object arg) {
  Object checked = Preconditions.checkNotNull(arg, "%s must not be null", "arg");
  checked.toString();
}

我们还可以使用 Lombok 来生成空值检测代码,并抛出带有提示信息的空指针异常:

public void testLombok(@NonNull Object arg) {
  arg.toString();
}

生成的代码如下:

public void testLombokGenerated(Object arg) {
  if (arg == null) {
    throw new NullPointerException("arg is marked @NonNull but is null");
  }
  arg.toString();
}

这个注解还可以用在类实例的成员变量上,所有的赋值操作会自动进行空值检测。

编程规范

通过遵守某些编程规范,也可以从一定程度上减少空指针异常的发生。

使用那些已经对 null 值做过判断的方法,如 String#equalsString#valueOf、以及三方库中用来判断字符串和集合是否为空的函数:

if (str != null && str.equals("text")) {}
if ("text".equals(str)) {}

if (obj != null) { obj.toString(); }
String.valueOf(obj); // "null"

// from spring-core
StringUtils.isEmpty(str);
CollectionUtils.isEmpty(col);
// from guava
Strings.isNullOrEmpty(str);
// from commons-collections4
CollectionUtils.isEmpty(col);

如果函数的某个参数可以接收 null 值,考虑改写成两个函数,使用不同的函数签名,这样就可以强制要求每个参数都不为空了:

public void methodA(Object arg1) {
  methodB(arg1, new Object[0]);
}

public void methodB(Object arg1, Object[] arg2) {
  for (Object obj : arg2) {} // no null check
}

如果函数的返回值是集合类型,当结果为空时,不要返回 null 值,而是返回一个空的集合;如果返回值类型是对象,则可以选择抛出异常。

Spring JdbcTemplate 正是使用了这种处理方式:

// 当查询结果为空时,返回 new ArrayList<>()
jdbcTemplate.queryForList("SELECT * FROM person");

// 若找不到该条记录,则抛出 EmptyResultDataAccessException
jdbcTemplate.queryForObject("SELECT age FROM person WHERE id = 1", Integer.class);

// 支持泛型集合
public <T> List<T> testReturnCollection() {
  return Collections.emptyList();
}

静态代码分析

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 加入到项目依赖中,以便使用对应的注解。

<dependency>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-annotations</artifactId>
    <version>3.1.7</version>
</dependency>

以下是对不同使用场景的说明:

@NonNull
private Object returnNonNull() {
  // 错误:returnNonNull() 可能返回空值,但其已声明为 @Nonnull
  return null;
}

@CheckForNull
private Object returnNullable() {
  return null;
}

public void testReturnNullable() {
  Object obj = returnNullable();
  // 错误:方法的返回值可能为空
  System.out.println(obj.toString());
}

private void argumentNonNull(@NonNull Object arg) {
  System.out.println(arg.toString());
}

public void testArgumentNonNull() {
  // 错误:不能将 null 传递给非空参数
  argumentNonNull(null);
}

public void testNullableArgument(@CheckForNull Object arg) {
  // 错误:参数可能为空
  System.out.println(arg.toString());
}

对于 Eclipse 用户,还可以使用 IDE 内置的空值检测工具,只需将默认的注解 org.eclipse.jdt.annotation.Nullable 替换为 SpotBugs 的注解即可:

Eclipse null analysis

Checker Framework 与 @NonNull、@Nullable

Checker Framework 能够作为 javac 编译器的插件运行,对代码中的数据类型进行检测,预防各类问题。

我们可以参照 官方文档,将 Checker Framework 与 maven-compiler-plugin 结合,之后每次执行 mvn compile 时就会进行检查。

Checker Framework 的空值检测程序支持几乎所有的注解,包括 JSR 305、Eclipse、甚至 lombok.NonNull

import org.checkerframework.checker.nullness.qual.Nullable;

@Nullable
private Object returnNullable() {
  return null;
}

public void testReturnNullable() {
  Object obj = returnNullable();
  // 错误:obj 可能为空
  System.out.println(obj.toString());
}

Checker Framework 默认会将 @NonNull 应用到所有的函数参数和返回值上,因此,即使不添加这个注解,以下程序也是无法编译通过的:

private Object returnNonNull() {
  // 错误:方法声明为 @NonNull,但返回的是 null。
  return null;
}

private void argumentNonNull(Object arg) {
  System.out.println(arg.toString());
}

public void testArgumentNonNull() {
  // 错误:参数声明为 @NonNull,但传入的是 null。
  argumentNonNull(null);
}

Checker Framework 对使用 Spring Framework 5.0 以上的用户非常有用,因为 Spring 提供了内置的空值检测注解,且能够被 Checker Framework 支持。

一方面我们无需再引入额外的 Jar 包,更重要的是 Spring Framework 代码本身就使用了这些注解,这样我们在调用它的 API 时就能有效地处理空值了。

举例来说,StringUtils 类里可以传入空值的函数、以及会返回空值的函数都添加了 @Nullable 注解,而未添加的方法则继承了整个框架的 @NonNull 注解,因此,下列代码中的空指针异常就可以被 Checker Framework 检测到了:

// 这是 spring-core 中定义的类和方法
public abstract class StringUtils {
  // str 参数继承了全局的 @NonNull 注解
  public static String capitalize(String str) {}

  @Nullable
  public static String getFilename(@Nullable String path) {}
}

// 错误:参数声明为 @NonNull,但传入的是 null。
StringUtils.capitalize(null);

String filename = StringUtils.getFilename("/path/to/file");
// 错误:filename 可能为空。
System.out.println(filename.length());

Optional 类型

Java 8 引入了 Optional<T> 类型,我们可以用它来对函数的返回值进行包装。

这种方式的优点是可以明确定义该方法是有可能返回空值的,因此调用方必须做好相应处理,这样也就不会引发空指针异常。

但是,也不可避免地需要编写更多代码,而且会产生很多垃圾对象,增加 GC 的压力,因此在使用时需要酌情考虑。

Optional<String> opt;

// 创建
opt = Optional.empty();
opt = Optional.of("text");
opt = Optional.ofNullable(null);

// 判断并读取
if (opt.isPresent()) {
  opt.get();
}

// 默认值
opt.orElse("default");
opt.orElseGet(() -> "default");
opt.orElseThrow(() -> new NullPointerException());

// 相关操作
opt.ifPresent(value -> {
  System.out.println(value);
});
opt.filter(value -> value.length() > 5);
opt.map(value -> value.trim());
opt.flatMap(value -> {
  String trimmed = value.trim();
  return trimmed.isEmpty() ? Optional.empty() : Optional.of(trimmed);
});

方法的链式调用很容易引发空指针异常,但如果返回值都用 Optional 包装起来,就可以用 flatMap 方法来实现安全的链式调用了:

String zipCode = getUser()
    .flatMap(User::getAddress)
    .flatMap(Address::getZipCode)
    .orElse("");

Java 8 Stream API 同样使用了 Optional 作为返回类型:

stringList.stream().findFirst().orElse("default");
stringList.stream()
    .max(Comparator.naturalOrder())
    .ifPresent(System.out::println);

此外,Java 8 还针对基础类型提供了单独的 Optional 类,如 OptionalIntOptionalDouble 等,在性能要求比较高的场景下很适用。

其它 JVM 语言中的空指针异常

Scala 语言中的 Option 类可以对标 Java 8 的 Optional

它有两个子类型,Some 表示有值,None 表示空。

val opt: Option[String] = Some("text")
opt.getOrElse("default")

除了使用 Option#isEmpty 判断,还可以使用 Scala 的模式匹配:

opt match {
  case Some(text) => println(text)
  case None => println("default")
}

Scala 的集合处理函数库非常强大,Option 则可直接作为集合进行操作,如 filermap、以及列表解析(for-comprehension):

opt.map(_.trim).filter(_.length > 0).map(_.toUpperCase).getOrElse("DEFAULT")
val upper = for {
  text <- opt
  trimmed <- Some(text.trim())
  upper <- Some(trimmed) if trimmed.length > 0
} yield upper
upper.getOrElse("DEFAULT")

Kotlin 使用了另一种方式,用户在定义变量时就需要明确区分 可空和不可空类型。当可空类型被使用时,就必须进行空值检测。

var a: String = "text"
a = null // 错误:无法将 null 赋值给非空 String 类型。

val b: String? = "text"
// 错误:操作可空类型时必须使用安全操作符(?.)或强制忽略(!!.)。
println(b.length)

val l: Int? = b?.length // 安全操作
b!!.length // 强制忽略,可能引发空值异常

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

相关文章

  • Java常见数据结构面试题(带答案)

    Java常见数据结构面试题(带答案)

    这篇文章主要介绍了Java常见数据结构面试题,带有答案及解释,希望对广大的程序爱好者有所帮助,同时祝大家有一个好成绩,需要的朋友可以参考下。
    2017-09-09
  • SpringMVC中的HandlerMapping详解

    SpringMVC中的HandlerMapping详解

    这篇文章主要介绍了SpringMVC中的HandlerMapping详解,HandlerMapping是请求映射处理器,也就是通过请求的url找到对应的逻辑处理单元(Controller),注意这里只是建立请求与Controller的映射关系,最终的处理是通过HandlerAdapt来进行处理的,需要的朋友可以参考下
    2023-09-09
  • mybatis之批量添加问题

    mybatis之批量添加问题

    这篇文章主要介绍了mybatis之批量添加问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • Java jvm中Code Cache案例详解

    Java jvm中Code Cache案例详解

    这篇文章主要介绍了Java jvm中Code Cache案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • IDEA的常见的设置和优化功能图文详解

    IDEA的常见的设置和优化功能图文详解

    这篇文章主要介绍了IDEA的常见的设置和优化功能,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-07-07
  • 基于SpringBoot实现图片上传与显示

    基于SpringBoot实现图片上传与显示

    这篇文章主要为大家详细介绍了基于SpringBoot实现图片上传与显示,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-08-08
  • java如何获取系统CPU、内存占用

    java如何获取系统CPU、内存占用

    这篇文章主要介绍了java如何获取系统CPU、内存占用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-10-10
  • Spring Boot 2.x 实现文件上传功能

    Spring Boot 2.x 实现文件上传功能

    这篇文章主要介绍了Spring Boot 2.x 实现文件上传功能,本文分步骤通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • SpringBoot实现单点登录的实现详解

    SpringBoot实现单点登录的实现详解

    在现代的Web应用程序中,单点登录(Single Sign-On)已经变得越来越流行,在本文中,我们将使用Spring Boot构建一个基本的单点登录系统,需要的可以参考一下
    2023-05-05
  • Java模拟实现机场过安检处理流程

    Java模拟实现机场过安检处理流程

    这篇文章主要为大家详细介绍了用Java模拟实现机场安全检查流程的案例,涉及线程的相关知识,由子线程不断的检查通过旅客,感兴趣的小伙伴们可以参考一下
    2022-04-04

最新评论