Java 中的 NullPointerException:原因和避免方法
NullPointerException:最常见的异常
NullPointerException (NPE) 是 Java 中最常见的异常。此异常的原因是已知的,但看起来在大多数情况下,开发人员更愿意忽略它并且不采取任何措施。我个人认为这种行为的原因如下:
大多数开发人员在这里没有看到任何问题,并将所有 NPE 异常视为开发人员的错。
意识到这个设计问题的开发人员不知道如何修复它。
在本文中,我将解释此问题的根源并提供解决该问题的方法。
问题的根源:Java 弱类型安全
Java 提供编译类型安全性,它向开发人员保证他不会错配不同的变量类型。而且,如果你这样做了 - 即使在编译步骤,Java 也会让他知道。在上面的示例中,我们尝试将 Integer 值分配给 String 变量:
空引用破坏了 Java 类型安全性
Java 在编译过程中验证变量的类型和赋值的类型。那么问题是什么?好吧,问题是 NULL 值。空值代表所有未初始化的对象。并且,只要可以初始化任何对象,那么可以将空值分配给任何类型。
所以,Java 允许下一个赋值:
这里有什么问题?对象未初始化,因此它们指向空引用。看似很自然,实则是大恶之源。
NullPointerException:弱类型安全的后果
就 Java 而言,Null 和真实对象之间没有区别,这会导致不可能的操作,如下面的下一个:
因此,从编译器的角度来看,没有任何问题。Null 属于 String 类型,Java 甚至不会打印警告。实际上,您甚至可以编译下一段代码:
但是,一旦我们运行这个程序,它就会失败并显示 NullPointerException:
空指针异常定义
NullPointerException 是一个运行时异常,当 Java 尝试调用真实对象上的任何方法时抛出,但在运行时此对象引用了 Null 引用。
为什么 NullPointerException 是最流行的异常?
开发人员是人类,习惯于犯错误忘记和匆忙。结果,他们错过了:
初始化对象
验证对象
使用用户地址示例解释 NullPointerException
在我们的示例中,我们有一个带有 Address 字段的 User 对象。有可能,它们都可能为空。让我们看看如何避免 NullPointerException。
使用 Simple != Null Check 避免 NullPointerException
现在,让我们通过简单的检查来防止这个问题,而不是空检查:
我们可以改进这个解决方案吗?是的,我们可以使用 Optional。使用 map 函数,我们可以编写类似于前面的语句的等效语句:
与简单的空检查相比,可选是否提供好处?是的,它确实。Optional 再次确保我们在 ifPresent lambda 中使用的数据不为空。但是,如果用户或地址为空怎么办?然后, ifPresent 将被默默忽略。
而且,即使我们忘记使用 Optional 功能,这个想法也会突出显示 .get() 提醒我们为设计提供空检查。
Optional 2014年发布,为什么不那么受欢迎?
可选特性是在 Java 1.8 中发布的,但并没有被广泛使用。有几个原因:
它非常冗长并且污染了代码(我个人认为这是主要原因,Java 本身非常冗长,而 Optional 变得非常大)。
不清楚,在所有 map/flatmap/ifpresent 后面你可能会失去逻辑的重点。所以丑陋的空检查是简单明了的。
Optional 本身可能会导致开发人员创建更多 NPE,例如通过使用 域名(nullable)。
因此,出于上述原因,一些团队更喜欢使用空检查。并且为了避免任何 NPE 异常,用一堆测试覆盖这样的逻辑。
Null Check 和 Optional 他们解决了问题吗?不。
上面显示了两个“解决方案”,它们真的是解决方案吗?空检查与 Optional 一起用于相同的目的——为可能为空的数据提供验证。另外,Optional 提醒开发者返回值可以为空。但是,总的来说,关键问题隐藏在人性中——忘记或错过潜在的空场景。我们需要一个解决方案来向开发人员指出他在编译步骤中遗漏了什么。
@NotNull @Nullable 注释处理器有助于识别潜在的空值。
我们需要一个解决方案,它可以在编译步骤读取我们的代码并通知我们我们错过了一个潜在的 NPE 场景。为此,我们可以使用 Java Annotation Processors。Java 注释处理器有很多用途,但也可用于我们的案例。
Lombok @NotNull 注释
Lombok @NotNull Annotation 用于生成可以阻止执行但仅限于 Runtime 的非空检查。 所以它不符合我们的目的。 很快,这个注解做了下一件事情:
检查器框架 @NonNull @Nullable 注释处理器
Checker Framework提供 @NonNull 和 @Nullable 注释以及可以识别潜在空检查的编译器处理器步骤。该框架可以通过强制开发人员指定 Nullability 来发现潜在的空值。所以,每当你返回一些东西时,你必须明确声明可以返回的结果是 Nullable 或 NotNullable ......让我们看看下一个例子:
一个可能返回 Null 而不是 String 的简单方法:
现在,让我们使用我们的 Checker 框架,看看它是否愿意编译它:
因此,它在第 19 行发现了一个潜在问题,我们尝试在 Nullable 字符串上调用 .length()。让我们使用 Null 检查和 Optional ifPresent 修复它:
并且,在编译之后,我们得到了一个成功的构建:
Checker 框架限制
到目前为止,Checker Framework 显示了良好的结果并突出了潜在的 NPE。但是,代价是什么?现在我们必须通过@Nullable 方法标记所有可能为 Nullable 的方法。似乎这是一个强制性步骤,我们无法避免。但是,这不是唯一的限制。让我们创建一个带有两个字段的简单类,其中一个字段我们标记为 @NonNull:
Checker Framework 会接受这个代码吗?
Checker 框架强制我们有一个初始化 id 值的构造函数,例如:
因此,Framework 不仅确定了潜在的 NPE,而且还迫使我们遵循特定的要求或设计。其实这个要求很关键。而且,这是有问题的。我们是否应该为了适应框架限制而牺牲这种开发灵活性?这是一个开放的问题。