如何避免 Java 中的“NullPointerException”
1
NullPointerException (NPE) 是 Java 中最常见的异常。此异常的原因是已知的,但在大多数情况下,开发人员更愿意忽略它并且不采取任何措施。我个人认为这种行为的原因如下:
大多数开发人员在这里没有看到任何问题,并将所有 NPE 异常都视为开发人员的错。
意识到这个设计问题的开发人员不知道如何解决它。
在本文中,我将解释这个问题的根源并提供解决该问题的方法。
2
你听说过编译类型安全吗?如果不在本文中,您可以了解它是什么以及编译时和类型安全之间的区别。
Java 提供了编译类型安全,它向开发人员保证他不能不匹配不同的变量类型。而且,如果您这样做了 - Java 甚至会在编译步骤中让他知道。在上面的示例中,我们尝试分配给 String 变量 Integer 值:
字符串变量
3
Java 在编译期间验证变量的类型和赋值的类型。那有什么问题呢?好吧,问题是 NULL 值。Null 值代表所有未初始化的对象。而且,只要可以初始化任何对象,就可以将 Null 值分配给任何类型。
因此,Java 允许下一个分配:
这里有什么问题?对象未初始化,因此它们指向空引用。看似很自然,实则是 万恶之源。
4
就 Java 而言,Null 和真实对象之间没有区别,它会导致不可能的操作,如下面的:不可能的操作。
所以,从编译器的角度来看,没有错。Null 属于 String 类型,Java 甚至不会打印警告。实际上,您甚至可以编译下一个代码:可编译的代码。
但是,一旦我们运行这个程序,它将失败并出现 NullPointerException:
空指针异常
5
NullPointerException 是一个运行时异常,当 Java 尝试调用真实对象上的任何方法但在运行时该对象引用 Null 引用时会引发该异常。您可以在本文中找到有关异常及其性质的更多详细信息。
6
开发人员是人类,总是习惯于忘记一些事情。因此,他们错过了:
初始化对象
验证对象
没有治愈人性的方法,也与它无关。避免NPE的实用方法是什么?让我们在下面回顾一个示例并尝试修复它。
7
在我们的示例中,我们有一个带有地址字段的用户对象。潜在地,它们都可能为空。让我们看看如何避免 NullPointerException。
潜在的空指针异常
8
现在,让我们通过简单的检查来防止这个问题,而不是空检查:
简单检查
我们可以改进这个解决方案吗?
是的,我们可以使用 Optional。使用 map 函数,我们可以编写与前面的语句类似的等价物:
与简单的空检查相比,可选是否提供好处?是的,它确实。Optional 向我们保证我们在 ifPresent lambda 中使用的数据不为空。但是,如果用户或地址为空怎么办?然后, ifPresent 将被静默忽略。
而且,即使我们忘记使用 Optional 功能,这个想法也会突出显示 .get() ,提醒我们为设计提供空检查。
9
可选功能在 Java 1.8 中发布,但并没有被广泛使用。有几个原因:
它非常冗长并且污染了代码(我个人认为这是主要原因,Java 本身非常冗长,而使用 Optional 它变得非常大)。
目前还不清楚,在所有 map/flatmap/ifpresent 背后,你可能会失去逻辑的意义。所以丑陋的空检查是简单明了的。
Optional 本身可能会导致开发人员创建更多的 NPE,例如通过使用 Optional.of(nullable)。
因此,出于上述原因,一些团队更喜欢使用空检查。为了避免任何 NPE 异常,用一堆测试来覆盖这样的逻辑。
10
上面显示了两个“解决方案”,它们真的是解决方案吗?Null 检查与 Optional 一起用于相同目的 - 为可能为 null 的数据提供验证。另外,Optional 提醒开发者返回值可以为空。但是,总的来说,关键问题隐藏在人性中——忘记或错过潜在的无效场景。我们需要一个解决方案来指出开发人员在编译步骤中遗漏了什么。
11
我们需要一个解决方案,它可以在编译步骤中读取我们的代码,并通知我们错过了潜在的 NPE 场景。为此,我们可以使用 Java 注释处理器。Java 注释处理器有很多用途,但也可以用于我们的案例。在本文中,您可以找到一个如何使用注释处理器来检查可变性的示例。
有几个与 NPE 问题相关的注释处理器。并非所有这些都是相同的,并且遵循完全不同的方法。
12
Lombok @NotNull Annotation 用于生成可以阻止执行但仅在 Runtime 中的非空检查。所以它不符合我们的目的。很快,这个注解做了接下来的事情:
注解
13
Checker Framework提供了 @NonNull 和 @Nullable 注释以及可以识别潜在空检查的编译器处理器步骤。该框架可以通过强制开发人员指定 Nullability 来找到潜在的空值。因此,每当您返回某些内容时,您必须显式声明返回的结果可以是 Nullable 还是 NotNullable...让我们看下一个示例:
一个可能返回 Null 而不是 String 的简单方法:
现在,让我们使用我们的 Checker 框架,看看它是否愿意编译它:
使用检查器框架
不,一点都不快乐。它说我们返回一个可能为空的字符串,并且它没有用 @Nullable 注释标记。现在,让我们将其标记为@Nullable,并尝试使用它:
使用 @Nullable 注释
该框架会在该代码中发现任何错误吗?让我们再次运行编译检查:
运行编译
因此,它在第 19 行发现了一个潜在问题,我们尝试在 Nullable 字符串上调用 .length()。让我们使用 Null 检查和可选的 ifPresent 来修复它:
使用 Null 检查和可选的 ifPresent 修复问题
而且,编译后,我们得到了一个成功的构建:
构建成功
15
到目前为止,Checker Framework 显示出良好的结果并突出了潜在的 NPE。但是,代价是什么?现在我们有义务通过@Nullable 方法标记所有可能为Nullable 的方法。这似乎是一个强制性的步骤,我们无法避免。但是,这不是唯一的限制。让我们创建一个简单的类,其中包含两个字段,其中一个是我们标记为@NonNull 的字段:
具有两个字段的简单类
Checker Framework 会接受此代码吗?
Checker Framework 强制我们有一个初始化 id 值的构造函数,例如:
构造函数
因此,Framework 不仅识别了潜在的 NPE,还迫使我们遵循特定的要求或设计。其实这个要求很关键。而且,这是值得怀疑的。我们是否应该为了适应框架限制而牺牲这种开发灵活性?这是一个悬而未决的问题。要使用 Checker Framework,您可以在此处获取我的示例:
git clone https://github.com/isicju/checker_framework_example
要运行 Checker Framework,请运行以下命令:
mvn clean compile
检查器框架替代方案:Intellij Idea @NotNull 注释
Checker Framework 不是唯一的解决方案,Intellij Idea 提供了自己的注释 @NotNull 和 @Nullable 以及嵌入在 IDE 插件中。不幸的是,我还没有找到在 maven 编译步骤中添加它的方法。因此,如果存在,请在评论中告诉我,我会对其进行测试并将其添加到文章中。
16
总结整篇文章,我建议如下:
更喜欢 Optional 而不是传递 Null
使用检查器框架
老实说,在实践中,Checker Framework 给您的开发带来了限制。如果我必须实现自己的解决方案并且它必须在生产中保持稳定,即使我必须摆脱 Lombok 甚至 Builder Pattern,我也会使用 Checker Framework。