Java mill 中更好的空值检查

2025-06-10

Java 中更好的空值检查

构建状态

照片由 Estée Janssens 在 Unsplash 上拍摄

Java 运行着世界上一些最大的网站和平台,但我经常对其语言的设计感到困惑。

然而,Java 确实是一种非常出色的语言,能够高效地完成工作。我在ExpediaOpenMarket等公司工作时,曾将 Java 投入生产环境,每秒处理数千个请求。Java 编译器以及围绕该语言的强大生态系统中提供的出色开发工具,可以有效防止各种应用程序错误。

遗憾的是,Java 应用程序中最常见的错误是 NullPointerException。每当 JVM 尝试取消引用变量时,如果发现值为 null 而不是对象,就会抛出NullPointerException。如何避免这种情况?答案很简单:不要让任何变量指向 null。

不幸的是,Java 是一种糟糕的语言,它实际上强制程序员创建(或通过方法参数接收)引用 null 的变量。任何已声明但未初始化的变量都会自动引用 null,而其他 Java 语言结构(例如 try/catch)则强制变量必须在外部作用域中声明,而 null 是唯一有效的选择之一。

下面是一个常见的例子,来自一个官方 Java 教程,该教程被认为是优秀的 Java:

public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering" + " try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
view raw WriteList.java hosted with ❤ by GitHub
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering" + " try statement");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println("Caught IndexOutOfBoundsException: "
+ e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
view raw WriteList.java hosted with ❤ by GitHub

如果之后有其他代码使用了 out 变量,则必须再次执行空值检查(如第 19 行所示)。如果频繁使用 out 变量,应用程序可能会充斥着大量的空值检查。

解决方法

我认为我有一个解决方案,但我想简要提一下现有的几种处理方法,以及为什么我认为我们仍然需要其他方法。

选项 1:不进行空检查。

一种想法是干脆不做空检查。等 NullPointerException 发生后再采取措施解决根本原因。Robert Brautingham 的《为什么我从不进行参数空检查》就是这种理念的一个例子。

当真正有可能解决根本原因时,我当然同意这种想法。不幸的是,我认为很多时候实际的语言会强制程序员进行空值检查。即使语言本身没有,其他库也会有。

照片由 Marvin Meyer 在 Unsplash 上拍摄
照片由 Marvin Meyer 在 Unsplash 上拍摄

选项 2:使用可选。

另一个选择(哈哈,明白了)是使用 Java 8 中引入的 Optional 类。Baeldung的《Java 8 Optional 指南》很好地介绍了它的用法。说实话,如果代码要处理 Stream,这主意不错。这也就是 Optional 的用途,如果你有这个选择(好吧,好吧,我知道这么说有点牵强),那就试试吧。

然而,我发现,在很多情况下,尴尬的空检查已经被仍然尴尬的 Optional 使用所取代,比如这个来自流行的开源 Java 工具的例子:

Optional<String> pathsStr = get(PATHS_KEY);
if (pathsStr.isPresent()) {
ObjectMapper objectMapper = new ObjectMapper();
try {
String[] paths = objectMapper.readValue(pathsStr.get(), String[].class);
// ... the rest elided
Optional<String> pathsStr = get(PATHS_KEY);
if (pathsStr.isPresent()) {
ObjectMapper objectMapper = new ObjectMapper();
try {
String[] paths = objectMapper.readValue(pathsStr.get(), String[].class);
// ... the rest elided

这与这个孪生代码中的空检查没什么不同:



String pathsStr = get(PATHS_KEY);
if (pathsStr != null) {
  ObjectMapper objectMapper = new ObjectMapper();
  try {
    String[] paths = objectMapper.readValue(pathsStr, String[].class);


Enter fullscreen mode Exit fullscreen mode

类似这样的例子使得很多 Java 社区成员认为Optional.get 是一种代码异味

更好的方法

其他语言(如 Groovy 和 C#)有一个很好的空条件运算符,允许程序员指定引用链可能在某处包含空值,而处理该问题的自然方法就是短路调用链并产生空值。

这是 C# 文档中的第一个示例:

int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null
int? count = customers?[0]?.Orders?.Count(); // null if customers, the first customer, or Orders is null
int? length = customers?.Length; // null if customers is null
Customer first = customers?[0]; // null if customers is null
int? count = customers?[0]?.Orders?.Count(); // null if customers, the first customer, or Orders is null

Java 不允许创建运算符,因此我们无法完全模仿这种行为。不过,我利用 Java 8 中的一些函数式特性(例如方法引用)创建了类似的功能。不妨看看。

举一个激励性的例子,在标准 Java 中,在用户的帐户中查找其邮政编码可能与此类似:

// standard java
String zipCode = null;
if(user != null) {
UserAddresses userAddresses = user.addresses();
if(userAddresses != null) {
Address billingAddress = userAddresses.billingAddress();
if (billingAddress != null) {
zipCode = billingAddress.zipCode();
}
}
}
// standard java
String zipCode = null;
if(user != null) {
UserAddresses userAddresses = user.addresses();
if(userAddresses != null) {
Address billingAddress = userAddresses.billingAddress();
if (billingAddress != null) {
zipCode = billingAddress.zipCode();
}
}
}

在十行空间中进行了三次空检查。

使用我的解决方案(已作为 Mill 库的一部分发布),可以将激励示例转换为:

String zipCode = NullSafe.of(user)
.call(User::addresses)
.call(UserAddresses::billingAddress)
.call(Address::zipCode)
.get();
String zipCode = NullSafe.of(user)
.call(User::addresses)
.call(UserAddresses::billingAddress)
.call(Address::zipCode)
.get();

之后会进行一次空值检查。或者,如果空字符串或其他默认值可能更适用,则可以使用 getOrDefault() 方法。此外,当更适合抛出异常时,还可以使用 getOrThrow() 方法。

照片由 Tanguy Sauvin 在 Unsplash 上拍摄
照片由 Tanguy Sauvin 在 Unsplash 上拍摄

Mill采用 MIT 许可证,因此您可以自由获取 jar 包并将其添加到应用程序的类路径中。Mill还具有许多实用的流相关功能,可以使流更加流畅、可读性更强

如果您愿意贡献代码,Mill 也欢迎您的贡献。代码库可以在 Github 上找到。我正在考虑将其发布出去,以便轻松添加到 Maven 或 Gradle 文件中。但所有事情都需要时间,并得到正确的帮助。

后记

我确信,只要 Java 还存在,Java 代码中就一定会有空值检查。问题是,我们能否做得比 Java 标准允许的更好?我相信答案是肯定的,我们可以。我希望通过结合本文提到的各种方法,包括 Mill 的 NullSafe 类,我们可以继续使我们的应用程序代码更加优雅,并防止 Java 应用程序中出现更多的 NullPointerException。

GitHub 徽标 scottshipp /磨坊

Java 库,让日常任务更加优雅。兼容 Java 8+。

构建状态

标识

Java 库使普通任务变得更加优雅。

注意:这个库已被弃用。

2020年3月更新。我将不再维护Mill。现在有其他库被更广泛地采用,并且有一些实际的资金支持和一些活跃的维护者。(参见StreamEx)。

此外,Mill现在是 Java 和 Scala 构建工具的名称。

尽管如此,它仍然很有用,因为它提供了一些可以添加到您自己的代码库中以改进 Java 流或 lambda 的工作的示例。

与 Java 8+ 兼容。

在此自述文件中

如何将 Mill 添加到您的应用程序中

如果您正在使用 Maven 或其他受支持的依赖项管理工具,则可以使用 Jitpack 将 Mill 添加到您的应用程序中。

Maven

首先,将 Jitpack(如果还没有)添加到存储库...

鏂囩珷鏉ユ簮锛�https://dev.to/scottshipp/better-null-checking-in-java-ngk
PREV
我从作家那里学到的五个编程技巧
NEXT
C 能做到的一切 Rust 都能做得更好