1. 概述

1965年,Tony Hoare提出了null引用的概念。自那时起,无数的时间都花在阅读日志和试图追踪空指针异常的源头上。这种异常如此常见,以至于通常简称为"NPE"。

在这篇教程中,我们将学习如何减轻这个问题。我们将回顾几种简化将null转换为默认值的方法。

2. 简单的if语句

处理转换的最简单方法是使用if语句。【它们是基础的编程结构,对不同经验和级别的开发者来说都很清晰易懂。】这种方法的优点在于其冗余性,这也可能是其缺点:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenIfWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = defaultValue;
    if (givenValue != null) {
        actual = givenValue;
    }
    assertDefaultConversion(givenValue, defaultValue, actual);
}

由于我们完全控制逻辑,可以轻松地修改、提取和重用它。此外,如果我们愿意,还可以使其惰性初始化

@ParameterizedTest
@ArgumentsSource(ObjectsSupplierProvider.class)
void givenIfWhenNotNullThenReturnsDefault(String givenValue, String expected, Supplier<String> expensiveSupplier) {
    String actual;
    if (givenValue != null) {
        actual = givenValue;
    } else {
        actual = expensiveSupplier.get();
    }
    assertDefaultConversion(givenValue, expected, actual);
}

如果操作相对简单,我们可以使用三元运算符使其更加内联。虽然Java没有引入Elvis运算符,但我们仍然可以改进代码:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenTernaryWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = givenValue != null ? givenValue : defaultValue;
    assertDefaultConversion(givenValue, defaultValue, actual);
}

同样,它允许惰性方式,因为只有所需的表达式会被计算:Java语言规范

我们可以将此逻辑提取到一个具有好名字的单独方法中,以提高代码可读性。然而,Java和一些外部库已经实现了这一点。

3. Java对象

Java 9为我们提供了两个实用方法:Objects.requireNonNullElseObjects.requireNonNullElseGet。这些方法的实现类似于我们之前审查的那些。总体而言,它们提供了一个更好的API,使代码更具自解释性:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenObjectsWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = requireNonNullElse(givenValue, defaultValue);
    assertDefaultConversion(givenValue, defaultValue, actual);
}

静态导入可以帮助我们移除Objects类名,减少噪音。懒惰版本看起来像这样:

@ParameterizedTest
@ArgumentsSource(ObjectsSupplierProvider.class)
void givenLazyObjectsWhenNotNullThenReturnsDefault(String givenValue, String expected,
  Supplier<String> expensiveSupplier) {
    String actual = requireNonNullElseGet(givenValue, expensiveSupplier);
    assertDefaultConversion(givenValue, expected, actual);
}

然而,这个API仅从Java 9开始可用。与此同时,Java 8也提供了一些方便的工具来达到类似的效果。

4. Java Optional<T>

Optional<T>类背后的主要思想是为了对抗null检查和NullPointerException的问题。可以在文档中识别出可变长参数接口(nullable APIs),但更好的解决方案是在代码中明确显示。从某个方法获取Optional<T>明确告诉我们值可能为null。此外,IDE可以通过静态分析提供通知和高亮显示。

Optional<T>类并没有设计用于进行显式的null检查。但是,我们可以使用它来包装我们想要检查的值,并在其上执行一些操作:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenOptionalWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = Optional.ofNullable(givenValue).orElse(defaultValue);
    assertDefaultConversion(givenValue, defaultValue, actual);
}

懒惰版本看起来非常相似:

@ParameterizedTest
@ArgumentsSource(ObjectsSupplierProvider.class)
void givenLazyOptionalWhenNotNullThenReturnsDefault(String givenValue, String expected,
  Supplier<String> expensiveSupplier) {
    String actual = Optional.ofNullable(givenValue).orElseGet(expensiveSupplier);
    assertDefaultConversion(givenValue, expected, actual);
}

null检查创建单独的包装对象可能有疑问。另一方面,这可能有助于在不进行链式null检查的情况下访问数据对象:Optional的链式调用

我们也可以使用Optional.map(Function<T, U>)做同样的事情:

public Delivery calculateDeliveryForPerson(Long id) {
    return Optional.ofNullable(getPersonById(id))
      .map(Person::getAddress)
      .map(Address::getZipCode)
      .map(ZipCode::getCode)
      .map(this::calculateDeliveryForZipCode)
      .orElse(Delivery.defaultDelivery());
}

尽早将对象包装在Optional<T>中可以减少后续必须进行的检查。

5. Guava库

如果上述所有方法都不适用,例如在使用较早版本的Java时,我们可以导入Guava以获得类似的功能:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>

它反映了Java的功能,没有添加任何明显有用的功能。为了在提供的值为null时获取默认值,我们可以使用MoreObjects

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenGuavaWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = MoreObjects.firstNonNull(givenValue, defaultValue);
    assertDefaultConversion(givenValue, defaultValue, actual);
}

MoreObjects取代了Guava的已弃用并计划删除的Objects实用类。但它不支持懒惰提供默认值。此外,它还提供了一个名为Optional<T>的类,与Java的同名,但位于不同的包中:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenGuavaOptionalWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = com.google.common.base.Optional.fromNullable(givenValue).or(defaultValue);
    assertDefaultConversion(givenValue, defaultValue, actual);
}

我们也可以使用这个类实现一系列修改操作:

@Override
public Delivery calculateDeliveryForPerson(Long id) {
    return Optional.fromNullable(getPersonById(id))
      .transform(Person::getAddress)
      .transform(Address::getZipCode)
      .transform(ZipCode::getCode)
      .transform(this::calculateDeliveryForZipCode)
      .or(Delivery.defaultDelivery());
}

transform方法不允许返回null的函数。因此,我们必须确保管道中的所有方法都不返回null。总的来说,如果Java功能不可用,Guava是一个很好的替代品,但它提供的功能比Java的Optional<T>少:过滤功能

6. Apache Commons

如果由于某种原因无法使用Java功能,我们还可以使用Apache Commons来简化null检查。添加依赖项:点击这里查看

然而,它仅提供从多个参数中获取第一个非null值的简单方法:

@ParameterizedTest
@ArgumentsSource(ObjectsProvider.class)
void givenApacheWhenNotNullThenReturnsDefault(String givenValue, String defaultValue) {
    String actual = ObjectUtils.firstNonNull(givenValue, defaultValue);
    assertDefaultConversion(givenValue, defaultValue, actual);
}

懒惰版本的API有点不方便,因为它需要Supplier<T>,所以如果我们已经有了值,就需要先包装起来:

@ParameterizedTest
@ArgumentsSource(ObjectsSupplierProvider.class)
void givenLazyApacheWhenNotNullThenReturnsDefault(String givenValue, String expected,
  Supplier<String> expensiveSupplier) {
    String actual = ObjectUtils.getFirstNonNull(() -> givenValue, expensiveSupplier);
    assertDefaultConversion(givenValue, expected, actual);
}

总的来说,如果Java功能不可用,这也是一个不错的替代方案。

7. 总结

NullPointerException是开发者最常遇到的异常。有许多便捷的方法来保证null安全。Java API和外部库提供了许多技术。然而,当简单地使用if语句时,没有什么可耻的,因为它清晰、简单且明确。

null检查的主要目标是尽早进行,并确保在整个项目中保持一致性。我们如何处理它并不重要。

如往常一样,教程的所有代码都可以在GitHub上找到。