如何避免 Java 中的“NullPointerException”

共 3558字,需浏览 8分钟

 ·

2022-04-24 23:25


1

最常见的异常


NullPointerException (NPE) 是 Java 中最常见的异常。此异常的原因是已知的,但在大多数情况下,开发人员更愿意忽略它并且不采取任何措施。我个人认为这种行为的原因如下: 


  • 大多数开发人员在这里没有看到任何问题,并将所有 NPE 异常都视为开发人员的错。 

  • 意识到这个设计问题的开发人员不知道如何解决它。 


在本文中,我将解释这个问题的根源并提供解决该问题的方法。



2

问题的根源:Java 弱类型安全


你听说过编译类型安全吗?如果不在本文中,您可以了解它是什么以及编译时和类型安全之间的区别。


Java 提供了编译类型安全,它向开发人员保证他不能不匹配不同的变量类型。而且,如果您这样做了 - Java 甚至会在编译步骤中让他知道。在上面的示例中,我们尝试分配给 String 变量 Integer 值:


字符串变量



3

空引用破坏了 Java 类型的安全性


Java 在编译期间验证变量的类型和赋值的类型。那有什么问题呢?好吧,问题是 NULL 值。Null 值代表所有未初始化的对象。而且,只要可以初始化任何对象,就可以将 Null 值分配给任何类型。



因此,Java 允许下一个分配: 



这里有什么问题?对象未初始化,因此它们指向空引用。看似很自然,实则是 万恶之源。



4

弱类型安全的后果


就 Java 而言,Null 和真实对象之间没有区别,它会导致不可能的操作,如下面的:不可能的操作。



所以,从编译器的角度来看,没有错。Null 属于 String 类型,Java 甚至不会打印警告。实际上,您甚至可以编译下一个代码:可编译的代码。



但是,一旦我们运行这个程序,它将失败并出现 NullPointerException:


空指针异常



5

NullPointerException 定义


NullPointerException 是一个运行时异常,当 Java 尝试调用真实对象上的任何方法但在运行时该对象引用 Null 引用时会引发该异常。您可以在本文中找到有关异常及其性质的更多详细信息。



6

为什么 NullPointerException 是最常见的异常? 


开发人员是人类,总是习惯于忘记一些事情。因此,他们错过了:


  • 初始化对象

  • 验证对象 


没有治愈人性的方法,也与它无关。避免NPE的实用方法是什么?让我们在下面回顾一个示例并尝试修复它。  



7

NullPointerException


在我们的示例中,我们有一个带有地址字段的用户对象。潜在地,它们都可能为空。让我们看看如何避免 NullPointerException。 


潜在的空指针异常



8

使用 != Null 检查避免


现在,让我们通过简单的检查来防止这个问题,而不是空检查:


简单检查


我们可以改进这个解决方案吗?


是的,我们可以使用 Optional。使用 map 函数,我们可以编写与前面的语句类似的等价物: 



与简单的空检查相比,可选是否提供好处?是的,它确实。Optional 向我们保证我们在 ifPresent lambda 中使用的数据不为空。但是,如果用户或地址为空怎么办?然后, ifPresent 将被静默忽略。 


而且,即使我们忘记使用 Optional 功能,这个想法也会突出显示 .get() ,提醒我们为设计提供空检查。 




9

Optional 为什么不那么受欢迎?

 

可选功能在 Java 1.8 中发布,但并没有被广泛使用。有几个原因:


  • 它非常冗长并且污染了代码(我个人认为这是主要原因,Java 本身非常冗长,而使用 Optional 它变得非常大)。

  • 目前还不清楚,在所有 map/flatmap/ifpresent 背后,你可能会失去逻辑的意义。所以丑陋的空检查是简单明了的。

  • Optional 本身可能会导致开发人员创建更多的 NPE,例如通过使用 Optional.of(nullable)。


因此,出于上述原因,一些团队更喜欢使用空检查。为了避免任何 NPE 异常,用一堆测试来覆盖这样的逻辑。



10

空检查和可选它们是否解决了问题?


上面显示了两个“解决方案”,它们真的是解决方案吗?Null 检查与 Optional 一起用于相同目的 - 为可能为 null 的数据提供验证。另外,Optional 提醒开发者返回值可以为空。但是,总的来说,关键问题隐藏在人性中——忘记或错过潜在的无效场景。我们需要一个解决方案来指出开发人员在编译步骤中遗漏了什么。



11

@NotNull @Nullable


我们需要一个解决方案,它可以在编译步骤中读取我们的代码,并通知我们错过了潜在的 NPE 场景。为此,我们可以使用 Java 注释处理器。Java 注释处理器有很多用途,但也可以用于我们的案例。在本文中,您可以找到一个如何使用注释处理器来检查可变性的示例。


有几个与 NPE 问题相关的注释处理器。并非所有这些都是相同的,并且遵循完全不同的方法。



12

Lombok @NotNull 注解


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。


浏览 16
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报