细说Java中的空指针异常

译文
开发 前端
本文将和您讨论空指针异常(Null Pointer Exception)的常见原因,以及如何避免它的各种优秀实践。

空指针(Null Pointer Exception,NPE)是Java中最常见不过的异常了。其原因虽然显而易见,但是开发人员往往会忽略,或未能及时采取措施。本文将和您详细讨论空指针问题的根源,以及对应的解决方法。

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

Java通过提供​​编译类型的安全性​​(Compile Type Safety),来保证开发人员不会错配不同的变量类型。在下面的示例中,我们试图将整形(Integer)值分配给某个字符串(String)变量,而Java会及时提醒您。

Java虽然会在编译过程中,去验证变量和赋值的类型,但是由于空值(NULL)代表了所有未初始化的对象,因此空值可以被分配为任何类型(如下图所示),且Java不会报错。

例如,Java允许如下赋值情况的出现:

这些对象在未被初始化的情况下,就指向了空引用,往往会产生Java类型的安全性漏洞。如下代码段所示,就Java而言,Null和真实对象可能并没有什么区别,但是它会导致一些不可实现的操作:

同时,由于Null属于String类型,因此在编译如下代码段时,Java甚至都不会有任何警告。

但是,我们一旦运行该程序代码,就会出现失败,并且会被提示如下的空指针异常:

空指针异常的定义

空指针异常属于运行时的异常。当Java尝试去调用真实对象上的任何方法时,如果在运行时中,该对象调用的是空引用(Null Reference),那么就会抛出异常。您可以通过链接--https://dzone.com/articles/java-exceptions-1,找到有关异常、及其根源的更多详细信息。

由于种种原因,开发人员时常会忘记初始化对象和验证对象。这往往是导致空指针异常的根源。下面让我们根据上述例子,讨论如何修复NPE。

解释空指针异常

下面是一个带有Address字段的User对象。它们都可能为空。

使用Simple != Null Check避免空指针异常

下面是通过简单的检查(并非Null Check),来防止该问题的发生:

作为改进方案,我们可以使用Optional,并通过map函数,编写出如下类似于前例的等效语句:

与简单的Null Check相比,Optional能够再次确保我们在ifPresent lambda中使用的数据不为空。这里的再次是指:如果User或Address的确为空的话,而且ifPresent被忽略了的话,即使我们忘记了使用Optional的相关功能,它也会以突出显示.get()的方式,并提醒我们为设计提供Null Check。

其实,早在2014年,Optional就作为可选特性,在Java 1.8中被发布了。不过由于如下原因,导致其至今未能被广泛地使用:

  • 由于Java本身非常冗长,因此Optional也跟着变得冗长起来,因此容易影响到代码的整体质量。
  • 对于各种map/flatmap/ifpresent逻辑,开发人员更倾向使用简单明了的Null Check。
  • Optional本身可能会导致开发人员创建更多NPE,例如使用到Optional.of(nullable)。

因此,鉴于上述原因,一些开发团队会更喜欢使用Null Check,并且会用一堆逻辑性的测试覆盖率,来避免潜在的NPE。

Null Check和Optional真的能够解决问题吗?

虽然上面讨论到的Null Check与Optional的使用目的都是针对空值数据进行验证。其中,Optional还可以提醒开发者返回值为空。但是,它们无法解决隐藏在开发者头脑中的关键问题——在编译步骤中出现的疏忽与遗漏。

@NotNull@Nullable注释处理器有助于识别潜在的空值

因此,我们需要一个解决方案,可以在编译步骤中读取代码,并通知开发人员他们可能疏漏的潜在NPE场景。对此,我们可以使用具有丰富功能的Java注释处理器(Annotation Processors)。您可以通过链接--https://www.javacodegeeks.com/2015/09/java-annotation-processors.html,了解如何使用注释处理器,来检查可变性的示例。

目前,业界有几种与NPE问题相关的注释处理器。它们并非遵循完全相同的方法。下面我们将重点讨论@NotNull和@Nullable两种注释提供工具。

Lombok的@NotNull注释

Lombok(译者注:一种Java库,提供了一组非常实用的注释)的@NotNull注释可用于生成那些仅在运行时(Runtime)阻断执行的非Null Check。下面的代码段展示了该注释、及其等效语句。

检查器框架的@NonNull和@Nullable注释处理器

​检查器框架​​(Checker Framework)提供了@NonNull和@Nullable注释,以及可以识别潜在Null Check的编译处理器的步骤。该框架可以通过强制开发人员指定的Nullability,来发现潜在的空值。因此,您的代码必须明确声明可返回的结果为Nullable或NotNullable。下面让我们来看一个可能返回Null,而非String的简单方法:

现在,让我们使用检查器框架,来检验是否可以完成编译。

如您所见,它报出了错误,并且返回了一个未使用@Nullable注释标记的疑似空字符串。那么,让我们将其标记为@Nullable试试:

如果我们再次运行编译检查,则会得到如下错误信息:

可见,检查器框架在第19行发现了一个潜在问题,即:我们在Nullable字符串上调用了.length()。下面,让我们使用Null Check和Optional的ifPresent来予以修复:

在编译之后,我们将得到如下成功的构建信息:

检查器框架的限制

至此,检查器框架向我们展示了良好的检查结果,并且突出了潜在的NPE。不过,其代价是我们必须通过@Nullable方法,标记所有可能为空的方法。为了突破该强制性的限制,我们可以创建一个带有两个字段的简单类,并且将其中一个字段标记为@NonNull:

下面是经由检查器框架的检查结果:

显然,检查器框架会强制要求我们构造一个初始化id值的构造函数,例如:

可见,检查器框架不仅能够识别潜在的NPE,而且还会迫使我们遵循特定的设计要求。这在某种程度上牺牲了框架开发的灵活性。如果您对该问题有兴趣的话,可以通过如下命令克隆我为您准备的示例:

git clone https://github.com/isicju/checker_framework_example


若要运行检查器框架的话,请使用如下命令:

Mvn clean compile


检查器框架的替代方案:Intellij Idea @NotNull注释

当然,检查器框架并非唯一的解决方案,Intellij Idea也提供了自己的注释--@NotNull和@Nullable,并嵌入到了IDE的插件中。目前,我尚未找到在maven编译步骤中添加它的方法。如果您对此有经验的话,欢迎您追加评论。

小结

通过上述讨论,我们可以看到,避免空指针异常的方法可以总结为:

  • 首选使用Optional,而不是传递Null
  • 使用检查器框架

当然,检查器框架会给您的开发带来一些限制。在实践中,如果您必须避免使用Lombok、甚至是Builder Pattern(建造者模式)的话,我建议您基于生产环境的稳定性考虑,去使用检查器框架。

译者介绍

陈 峻 (Julian Chen),51CTO社区编辑,具有十多年的IT项目实施经验,善于对内外部资源与风险实施管控,专注传播网络与信息安全知识与经验;持续以博文、专题和译文等形式,分享前沿技术与新知;经常以线上、线下等方式,开展信息安全类培训与授课。

【原标题】Null Pointer Exception in Java: Causes and Ways to Avoid It(作者:Dmitry Egorov)


责任编辑:华轩 来源: 51CTO
相关推荐

2015-07-30 10:12:32

JavaNullAssert

2023-10-08 11:09:22

Optional空指针

2024-02-28 09:03:20

Optional空指针Java

2024-02-01 12:09:17

Optional容器null

2021-02-28 21:47:51

Java语法糖算数

2021-05-12 07:03:25

Switch报空指针

2015-01-05 10:01:20

Java

2021-11-15 06:56:45

系统运行空指针

2024-08-12 08:28:35

2020-11-02 07:43:33

前端空指针数据

2022-01-04 23:13:57

语言PanicGolang

2009-09-28 11:25:17

PersistentQKestrelScala

2024-10-15 15:58:11

2009-06-25 14:05:40

Java应用技巧

2020-07-02 22:42:18

Java异常编程

2011-04-11 11:09:50

this指针

2015-03-16 10:33:14

Swift指针

2013-04-07 10:01:26

Java异常处理

2015-01-21 16:25:29

Swift指针

2009-04-10 09:43:00

Java输出流异常
点赞
收藏

51CTO技术栈公众号