1、简介 {#1简介}
NullPointerException (空指针异常)是 Java 最常见的异常之一。它发生在访问或与指向空(null
)的引用变量交互时。验证对象非空(尤其是当它们是方法或构造函数中的参数时)对于确保代码的健壮性和可靠性非常重要。我们可以通过编写自己的 null
检查代码、使用第三方库或选择更方便的方法来实现这一点。
本文将带你了解 Java 中内置的一种灵活的解决方案 - Objects.requireNonNull()
。
2、Null 值处理 {#2null-值处理}
简单回顾一下,有很多方法可以避免手动空值检查。我们可以从各种库中进行选择,而不是用 if
语句来封装我们的代码,这可能会导致错误和耗时。Spring、Lombok(@NonNull
) 和 Uber 的 NullAway
就是其中的几种。
相反,如果我们想保持一致性,避免在代码库中引入额外的库,或者使用普通 Java,我们可以选择 Optional
类或 Objects
方法。虽然 Optional
可简化空值处理,但它往往会增加编码开销,并使简单用例的代码变得杂乱无章。由于它是一个独立的对象,因此也会消耗额外的内存。此外,Optional
只能将 NullPointerException
转换为 NoSuchElementException
,即不能解决缺陷。
而,Objects
类提供了静态工具方法,可以更高效地处理对象。该类在 Java 7 中引入,并在 Java 8 和 Java 9 中多次更新,它还提供了在执行操作前验证条件的方法。
3、Objects.requireNonNull() 的优点 {#3objectsrequirenonnull-的优点}
Objects
类通过提供一组 requireNonNull()
静态方法,简化了对 null
值的检查和处理。这些方法提供了一种简单明了的方法,可在使用前检查对象是否为空。
requireNonNull()
方法的主要目的是检查对象引用是否为空,如果为空,则抛出 NullPointerException
异常。通过明确抛出异常,我们传达了检查的意图,使代码更易于理解和维护。这也会告知开发人员和维护人员,该错误是故意的,这有助于防止随着时间的推移对行为进行无意的更改。
Objects
类是 java.util
包的一部分,因此无需外部库或依赖关系即可轻松访问。它的方法都有详细的文档说明,为开发人员提供了正确使用的明确指导。
4、使用案例 {#4使用案例}
现在,来看看 requireNonNull()
方法的各种用例(包括其各种重载方法)。
这些方法允许在不同情况下处理空值,从简单的空值检查到带有自定义信息的更复杂的验证。
此外,还有两个方法支持默认值 - requireNonNullElse()
和 requireNonNullElseGet()
。
4.1、单参数验证 {#41单参数验证}
首先,来看看最简单的实现 - 单个参数的 requireNonNull()
:
public static <T> T requireNonNull(T obj) {
if (obj == null) {
throw new NullPointerException();
} else {
return obj;
}
}
该方法会检查提供的对象引用是否为空。如果是,则抛出 NullPointerException
异常。否则,返回未更改的对象。
该方法主要用于方法和构造函数中的参数验证。例如,为了确保传给 greet()
方法的名称不是空值,我们可以使用下面的方法:
void greet(String name) {
Objects.requireNonNull(name, "Name cannot be null");
logger.info("Hello, {}!", name);
}
@Test
void givenObject_whenGreet_thenNoException() {
Assertions.assertDoesNotThrow(() -> greet("Baeldung"));
}
@Test
void givenNull_whenGreet_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> greet(null));
}
通过使用 requireNonNull()
,我们可以确保方法收到有效参数。我们可以在使用 null
对象之前检测到它们,从而避免在访问这些对象时出现错误。
4.2、使用自定义错误信息 {#42使用自定义错误信息}
Objects.requireNonNull()
的一个重载方法在检查对象引用是否为空的同时接受一个字符串消息。这样,如果对象引用为空,就可以抛出带有自定义错误信息的 NullPointerException
:
public static <T> T requireNonNull(T obj, String message) {
if (obj == null)
throw new NullPointerException(message);
return obj;
}
我们可以使用这种方法在带有多个参数的构造函数中进行验证:
static class User {
private final String username;
private final String password;
public User(String username, String password) {
this.username = Objects.requireNonNull(username, "Username is null!");
this.password = Objects.requireNonNull(password, "Password is null!");
}
// getters 省略
}
@Test
void givenValidInput_whenNewUser_thenNoException() {
Assertions.assertDoesNotThrow(() -> new User("Baeldung", "Secret"));
}
@Test
void givenNull_whenNewUser_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> new User(null, "Secret"));
Assertions.assertThrows(NullPointerException.class, () -> new User("Baeldung", null));
}
带有自定义消息的 NullPointerException
可提供更多上下文信息,具体说明出错的原因和导致问题的参数。这反过来又改进了错误报告,使调试变得更容易。
4.3、通过 Supplier 延迟创建消息 {#43通过-supplier-延迟创建消息}
最后,还可以使用另一种重载方法来自定义 NullPointerException
:
public static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null) {
throw new NullPointerException(messageSupplier == null ? null : (String)messageSupplier.get());
} else {
return obj;
}
}
第二个参数是错误信息的 Supplier
。它允许在 null
值检查之后再创建信息,从而提高性能。这对于成本较高的操作(如字符串连接或复杂计算)尤为重要:
void processOrder(UUID orderId) {
Objects.requireNonNull(orderId, () -> {
String message = "Order ID cannot be null! Current timestamp: " + getProcessTimestamp();
message = message.concat("Total number of invalid orders: " + getOrderAmount());
message = message.concat("Please provide a valid order.");
return message;
});
logger.info("Processing order with id: {}", orderId);
}
private static int getOrderAmount() {
return new Random().nextInt(100_000);
}
private static Instant getProcessTimestamp() {
return Instant.now();
}
@Test
void givenObject_whenProcessOrder_thenNoException() {
Assertions.assertDoesNotThrow(() -> processOrder(UUID.randomUUID()));
}
@Test
void givenNull_whenProcessOrder_thenException() {
Assertions.assertThrows(NullPointerException.class, () -> processOrder(null));
}
在上面的示例中,getOrderAmount()
和 getProcessTimestamp()
可能涉及数据库查询或外部 API 调用等耗时操作。通过推迟消息创建,当 orderId
不为空时,这种方法可以避免不必要的性能成本。
不过,必须确保创建信息 Supplier
的开销低于直接生成字符串信息的开销。
5、最佳实践 {#5最佳实践}
正如我们前面所看到的,在设计方法和构造函数时,我们需要强制执行参数限制,以确保代码行为的可预测性。在方法或构造函数的开头使用 Objects.requireNonNull()
有助于及早捕获无效参数。这种做法还能使我们的代码保持整洁,更易于维护和调试。
更重要的是,requireNonNull()
对于构建一个快速失效系统至关重要。快速出错原则意味着可以立即检测到错误,防止出现连锁故障并降低调试的复杂性。如果一个方法预先验证了其参数,那么它很快就会出现明显的异常,使问题的根源一目了然。这些检查对于系统避免产生混乱的错误、不正确的结果,甚至避免代码中无关部分出现问题是非常必要的。
另一个好的做法是明确记录 public
或 protected
方法的参数限制。我们可以使用 Javadoc @throws
标签来指定在违反参数限制时抛出的异常。如果多个方法都抛出 NullPointerException
,我们可以在类级 Javadoc 中进行说明,而不必在每个方法中重复说明。
6、总结 {#6总结}
本文介绍了如何使用 Objects.requireNonNull()
(及其重载)方法有效地验证方法和构造函数参数。这些方法为在 Java 中处理 null
检查提供了一种简单而强大的方法。内置的自定义错误消息和延迟消息创建支持增加了灵活性,使其适用于各种用例。
此外,采用最佳实践,如执行参数限制、使用快速失效原则和记录异常情况,也可以提高代码库的整体质量和可维护性。
Ref:https://www.baeldung.com/java-objects-requirenonnull