Java 14中HelpfulNullPoInterExceptions
1. 概述
在本教程中,我们将通过查看HelpfulNullPointerException来继续我们关于 Java 14 的系列,这是此版本的 JDK 引入的新功能。
2. 传统的NullPointerException
在实践中,我们经常看到或编写代码将 Java 中的方法链接起来。但是,当这段代码抛出NullPointerException 时,就很难知道异常源自何处。 假设我们想找出员工的电子邮件地址:
String emailAddress = employee.getPersonalDetails().getEmailAddress().toLowerCase();
如果employee对象getPersonalDetails()或getEmailAddress()为Null,则 JVM 抛出NullPointerException:
Exception in thread "main" java.lang.NullPointerException
at com.blogdemo.java14.npe.HelpfulNullPointerException.main(HelpfulNullPointerException.java:10)
异常的根本原因是什么?不使用调试器很难确定哪个变量为Null。而且,JVM 只会打印出导致异常的方法、文件名和行号。
在下一节中,我们将看看 Java 14 如何通过 JEP 358 解决这个问题。
3. HelpfulNullPointerException
SAP在 2006 年为其商业 JVM 实施了HelpfulNullPointerException 。它于 2019 年 2 月被提议作为 OpenJDK 社区的增强功能,此后很快就成为了 JEP。因此,该功能已于 2019 年 10 月完成并推送到 JDK 14 版本中。
本质上,JEP 358 旨在通过描述哪个变量为null来提高 JVM 生成的NullPointerException的可读性。
JEP 358通过描述null变量、方法、文件名和行号来带来详细的NullPointerException消息。它通过分析程序的字节码指令来工作。因此,它能够准确地确定哪个变量或表达式为null。 最重要的是,在 JDK 14 中默认关闭详细的异常消息。要启用它,我们需要使用命令行选项:
-XX:+ShowCodeDetailsInExceptionMessages
3.1. 详细的异常消息
让我们考虑在激活ShowCodeDetailsInExceptionMessages标志的情况下再次运行代码:
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.toLowerCase()" because the return value of
"com.blogdemo.java14.npe.HelpfulNullPointerException$PersonalDetails.getEmailAddress()" is null
at com.blogdemo.java14.npe.HelpfulNullPointerException.main(HelpfulNullPointerException.java:10)
这一次,从附加信息中,我们知道缺少员工个人详细信息的电子邮件地址导致了我们的异常。从这个增强中获得的知识可以在调试过程中节省我们的时间。 JVM 由两部分组成详细的异常消息。第一部分表示失败的操作,这是引用为null的结果,而第二部分标识了null引用的原因:
Cannot invoke "String.toLowerCase()" because the return value of "getEmailAddress()" is null
为了构建异常消息,JEP 358 重新创建了将null引用推送到操作数堆栈的源代码部分。
3.2. 技术方面
现在我们已经很好地理解了如何使用HelpfulNullPointerException识别null引用,让我们来看看它的一些技术方面。
首先,**只有当 JVM 本身抛出 NullPointerException 时才会进行详细的消息计算——如果我们在 Java 代码中显式抛出异常,则不会执行计算。**这背后的原因是,在这些情况下,很可能我们已经在异常构造函数中传递了有意义的消息。
其次,JEP 358 延迟计算消息,这意味着仅在我们打印异常消息时而不是在异常发生时计算。因此,对于通常的 JVM 流程,我们捕获并重新抛出异常不应该有任何性能影响,因为我们并不总是打印异常消息。
最后,详细的异常消息可能包括我们源代码中的局部变量名。因此,我们可以认为这是一个潜在的安全风险。但是,这仅在我们运行在激活*-g*标志的情况下编译的代码时发生,它会生成调试信息并将其添加到我们的类文件中。
考虑一个简单的示例,我们编译它以包含此附加调试信息:
Employee employee = null;
employee.getName();
当我们运行这段代码时,异常消息会打印局部变量名称:
Cannot invoke
"com.blogdemo.java14.npe.HelpfulNullPointerException$Employee.getName()"
because "employee" is null
相反,如果没有额外的调试信息,JVM 只会在详细消息中提供它所知道的有关变量的信息:
Cannot invoke
"com.blogdemo.java14.npe.HelpfulNullPointerException$Employee.getName()"
because "<local1>" is null
JVM 打印由编译器分配的变量索引,而不是局部变量名(employee ) 。