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

相关文章

  • spring系列笔记之常用注解

    spring系列笔记之常用注解

    这篇文章主要给大家介绍了关于spring系列笔记之常用注解的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用spring具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-04-04
  • 一文带你深入剖析Java线程池的前世今生

    一文带你深入剖析Java线程池的前世今生

    这篇文章主要带大家介绍了深入剖析一下Java线程池的前世今生,了解线程池的原理以及为什么需要线程池。文中的示例代码讲解详细,需要的可以参考一下
    2022-10-10
  • java中的十个大类总结

    java中的十个大类总结

    java.lang.string字符串类将是无可争议的冠军在任何一天的普及和不可以否认。这是最后一个类,用来创建操作不可变字符串字面值
    2013-10-10
  • SpringCloud中的OpenFeign调用解读

    SpringCloud中的OpenFeign调用解读

    OpenFeign是一个显示声明式的WebService客户端,使用OpenFeign能让编写Web Service客户端更加简单OpenFeign的设计宗旨式简化Java Http客户端的开发,本文给大家介绍SpringCloud之OpenFeign调用解读,感兴趣的朋友一起看看吧
    2023-11-11
  • SpringBoot使用AOP记录接口操作日志详解

    SpringBoot使用AOP记录接口操作日志详解

    这篇文章主要为大家详细介绍了SpringBoot使用AOP记录接口操作日志,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • Spring Boot中的那些条件判断的实现方法

    Spring Boot中的那些条件判断的实现方法

    这篇文章主要介绍了Spring Boot中的那些条件判断的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • Java设计模式之备忘录模式(Memento模式)介绍

    Java设计模式之备忘录模式(Memento模式)介绍

    这篇文章主要介绍了Java设计模式之备忘录模式(Memento模式)介绍,memento是一个保存另外一个对象内部状态拷贝的对象,这样以后就可以将该对象恢复到原先保存的状态,需要的朋友可以参考下
    2015-03-03
  • Java8的default方法详细介绍

    Java8的default方法详细介绍

    这篇文章主要介绍了Java8的default方法,详细介绍了什么是default方法,在多继承时的处理等,需要的朋友可以参考下
    2014-04-04
  • 全面解读Java NIO(看这篇就够了)

    全面解读Java NIO(看这篇就够了)

    Java NIO是Java1.4之后推出来的一套IO接口,NIO提供了一种完全不同的操作方式, NIO支持面向缓冲区的、基于通道的IO操作,这篇文章主要介绍了Java NIO详解(看这篇就够了),需要的朋友可以参考下
    2023-05-05
  • Java设计模式之中介模式(Mediator模式)介绍

    Java设计模式之中介模式(Mediator模式)介绍

    这篇文章主要介绍了Java设计模式之中介模式(Mediator模式)介绍,本文讲解了为何使用Mediator模式、如何使用中介模式等内容,需要的朋友可以参考下
    2015-03-03

最新评论