本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
NPE 让我们深夜加班,JEP 358 救我们的命,但“?.”操作符却被 Oracle,也就是 Java “拒之门外”。
每个 Java 程序员都曾被#NullPointerException(NPE)支配过,尤其是初入行的时候。1 行看似简单的user.getGroup().getLeader().getDeptName()代码,一旦抛出 NPE,不少人曾经对着堆栈信息抓耳挠腮,猜测到底是 user、group 还是 leader 为 null?
好在从 Java 14 引入的 JEP 358 特性之后,终结了这场“猜谜游戏”,但是这并没有解决 null 带来的 NPE 问题。Java 反而引入了一个颇具争议的 Optional,试图让 NPE 问题得到更稳妥的处理,但是这个 Optional 在社区里的“名声”非常不好,被多数人称为鸡肋。具体可以参考我的这些文章《https://mp.weixin.qq.com/s/gdvGqd0NSae9Y1vne99uVQ》、《https://mp.weixin.qq.com/s/Ard-MCSFITVcvyL71XQchQ》。
面对这种,1 行代码,三个 null,Kotlin 有 “?.”,Java 为什么没有?.,这个安全导航操作符至今仍未加入 Java 大家庭的原因是什么?今天我们就一起来聊一聊。
JEP 358
Java 14 发布的时候,我看到了 JEP 358,我本以为看到了希望,这些年才知道,我看到的是失望🤣。
这个提案要解决的问题是,详细精确的指出是具体是哪个变量或表达式为 null。
在这个提案之前,比如类似下面这个异常。
Exception in thread "main" java.lang.NullPointerException
at com.xttblog.NpeTest.main(NpeTest.java:5)
我们对这个空异常破案时,就像侦探破案只给了你一个案发地点(第 5 行),却没有任何关于凶手(哪个变量为 null)的线索。对于a.b.c.i = 99;这样的链式调用,根本无法从堆栈中判断是 a、b 还是 c 为 null。通常,我们只能通过添加日志、断点调试,甚至重构代码来拆分调用链,才能定位问题。这对于线上故障排查来说,效率极低。
于是,Java 为了解决这个长期痛点,OpenJDK 在 Java 14 中正式引入了#JEP 358: Helpful NullPointerExceptions #业余草(友好的空指针异常)。
这项特性让 JVM 在抛出 NPE 时,不再只是冷冰冰地告诉你“出事了”,而是会提供一份详细的“目击者证词”,精确指出是哪个变量或表达式为 null。
// 代码: a.b.c.i = 99;
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "c" because "a.b" is null
at NpeTest.main(NpeTest.java:5)
Kotlin 的 “?.”
用过 Kotlin,或见过 Kotlin 的应该知道,它有一个?.式的语法。支持下面这样的链式调用。
// 即使 return null 返回空,也不会抛出 NPE
String text = user.getGroup()?.getLeader()?.getDeptName();
val deptName = user?.group?.leader?.deptName
这就是 Elvis 表达式 (Null Coalescing)给它的一个兜底。
类似?.这样的安全导航操作符(Safe Navigation Operator)在 Groovy、Kotlin、JavaScript、C#、Swift、Ruby 中都得到了类似的支持,然而 Java 却很头铁,它不支持。
事实上,在 Java 7 的早期讨论中,就曾有人提议加入类似“Elvis 操作符”的语法糖,但最终被拒绝了。具体的细节参考这篇文章https://mail.openjdk.org/pipermail/coin-dev/2009-March/000047.html。
Java 拒绝的原因
社区里总结了不少 Java 拒绝的原因,但我认为这很可能都是表面原因。
更深层次的原因是,java 7 那个时候,面临着被收购,以及众多特性难产的原因,没有余力来实现一个极其复杂的操作符。
要知道,那个时候,Java 7 延期、延期,还是延期,包括 lambda 在内的众多特性都砍掉,放到 Java 8 了。
所以,那个时候,Java 团队给出的原因,我一点也不觉得是阻碍。
- 语言哲学:Java 的设计哲学倾向于显式和明确。核心开发团队认为,使用 Optional 类或传统的 if 判断虽然代码略显冗长,但逻辑更清晰,意图更明确。
- 已有替代方案:Java 8 引入的 Optional 类就是官方推荐的处理可能为 null 的值的方式。虽然它在处理深层嵌套时依然不够优雅,但它提供了一种函数式的、链式调用的解决方案。
- 兼容性与语法复杂性:向一门成熟且庞大的语言中添加新的操作符,需要考虑其与现有语法、泛型、类型推断等特性的交互,评估其带来的复杂性是否值得。
- Java 的核心设计原则之一: “显式优于隐式”(Explicit is better than implicit)。
总之,这类?.安全导航操作符或可选链操作符,在 Java 7 时期就被拒绝了。那时,就有社区提议引入类似?.或 ?:(Elvis 操作符)的语法糖,但都被当时的语言团队(由 Gilad Bracha 等人主导)明确拒绝,理由之一正是“破坏语言一致性”和“鼓励不良实践”。
Java 推荐方案
就这样,一些 Java 热爱者由粉转黑了。而官方推荐拥抱 Optional,原因之一就是我说的,它实现起来更简单。
String deptName = Optional.ofNullable(user)
.map(User::getGroup)
.map(Group::getLeader)
.map(Leader::getDeptName)
.orElse("Unknown");
更有“恶心”的人,封装了一个搞笑的工具。
public static <T> T safeGet(Supplier<T> supplier, T defaultValue) {
try {
return supplier.get();
} catch (NullPointerException e) {
return defaultValue;
}
}
// 使用
String deptName = safeGet(() -> user.getGroup().getLeader().getDeptName(), "业余草");
当然,也有一些第三方框架或三方库,如 Vavr(以前叫 Javaslang),提供了更强大的 Option 类型和模式匹配,能写出更接近函数式语言的代码。
总结
我个人觉得类似?.这类安全导航操作符(Safe Navigation Operator),也有人称为 “空感知操作符” 或 “可选链操作符(Optional Chaining Operator)”,没有被正式推出,是有一部分历史原因的。
而现在的 Java 语言架构师(如 Brian Goetz)也多次在公开场合表示,他们更关注模式匹配、值类型、泛型增强等“结构性改进”,而非增加“语法糖”。
社区里,虽然也有一小部分呼声,但未形成足够推动力。相反,大家认为在云原生时代, Project Loom(虚拟线程)、Project Valhalla(值类型)等重大特性的优先级更高,?.的优先级被遗忘到了角落。更何况现在有了 Option,?.这类特性或许难有“出头之日”。
参考资料
https://mail.openjdk.org/pipermail/coin-dev/2009-March/000047.htmlhttps://openjdk.org/jeps/358https://en.wikipedia.org/wiki/Safe_navigation_operatorhttps://stackoverflow.com/questions/61042145/java-14-nullpointerexception-no-detailed-messagehttps://vavr.iohttps://www.baeldung.com/java-8-elvis-operator-implementation

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » 1 行代码,三个 null,Java 拒绝了 Elvis 操作符