当前位置: 主页 > JAVA语言

java空指针异常处理-java空指针异常的原因

发布时间:2023-02-09 09:10   浏览次数:次   作者:佚名

Java 中的任何对象都可能是空的。 当我们调用空对象的方法时,会抛出 NullPointerException,这是一种很常见的错误类型。 我们可以使用几种方法来避免这种异常,使我们的代码更加健壮。 本文将列举这些解决方案,包括传统的空值检测、编程标准,以及使用现代 Java 语言引入的各种工具作为辅助。

运行时检测

最明显的方法是使用 if (obj == 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();
}

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

编程规范

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

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);

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

// 当查询结果为空时,返回 new ArrayList<>()
jdbcTemplate.queryForList("SELECT * FROM person");
// 若找不到该条记录,则抛出 EmptyResultDataAccessException
jdbcTemplate.queryForObject("SELECT age FROM person WHERE id = 1", Integer.class);
// 支持泛型集合
publicListtestReturnCollection() {
  return Collections.emptyList();
}

静态代码分析

Java语言的静态代码分析工具有很多,如Eclipse IDE、SpotBugs、Checker Framework等,可以帮助程序员在编译时发现错误。 结合@Nullable、@Nonnull等注解,我们可以找到程序运行前可能抛出空指针异常的代码。

但是,空检测注释尚未标准化。 尽管 JSR 305 规范于 2006 年 9 月由社区提出,但长期以来一直搁置。 许多第三方库提供类似的注释,并由不同的工具支持,其中包括:

我建议使用跨 IDE 解决方案,例如 SpotBugs 或 Checker Framework,它们都与 Maven 集成得很好。

带有@NonNull、@CheckForNull 的 SpotBugs

SpotBugs 是 FindBugs 的继承者。 通过在方法参数和返回值上添加@NonNull和@CheckForNull注解,SpotBugs可以帮助我们在编译时进行空值检测。 需要注意的是SpotBugs不支持@Nullable注解,必须换成@CheckForNull。 如官方文档所述,@Nullable 仅在需要覆盖@ParametersAreNonnullByDefault 时使用。

官方文档解释了如何将 SpotBugs 应用于 Maven 和 Eclipse。 我们还需要在项目依赖中添加spotbugs-annotations,以便使用相应的注解。

com.github.spotbugsspotbugs-annotations3.1.7

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

@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注解即可:

7658700a3769acfb75302eb334e74afc

带有@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注解整个框架。 因此java空指针异常处理,在下面的代码中,Checker Framework 可以检测到 Null 指针异常:

// 这是 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());

可选类型

Java 8 引入了 Optional 类型,我们可以用它来包装函数的返回值。 这种方法的好处是可以明确定义该方法可能会返回一个空值,所以调用者必须进行相应的处理,才不会抛出空指针异常。 但是难免需要写更多的代码,会产生大量的垃圾对象,增加GC的压力,所以在使用的时候需要考虑。

Optionalopt;
// 创建
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 8Stream API 也使用 Optional 作为返回类型:

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

此外,Java 8还为基本类型提供了单独的Optional类,比如OptionalInt、OptionalDouble等,非常适合对性能要求比较高的场景。

其他 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可以直接作为集合来操作,比如filer、map、list comprehension(for-comprehension):

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

Kotlin使用了另一种方式,用户在定义变量时需要明确区分。 使用可空类型时java空指针异常处理,需要进行空检查。

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注解,这样可以强制调用者防止空指针异常,让我们的程序更加健壮。

参考