本文最初发布于 literatejava.com 网站,经原作者授权由 InfoQ 中文站翻译并分享。
检查异常(checked exception)一直是 Java 语言中一个有争议的特性。
拥护者声称,这一特性可以确保故障检查并从故障中恢复。而批评者则说,“catch”块几乎永远无法从异常中恢复,并且是错误的常见来源。
今天,Java 8 和 lambda 已经出现了。在 Java 世界中,检查异常是否已经过时了呢?
检查异常的意图
在 90 年代中期,Sun 公司的 James Gosling 提出了一种新的语言。
当时,C++编程要求每个返回的函数都要检查是否有错误。他认为必须找出更好的方法,并因此将“异常”的概念构建到 Java 语言中。
检查异常的目的是在本地标志,并强制开发人员处理可能的异常。已检查的异常必须在方法签名上声明或处理。
这是为了增强软件的可靠性和弹性。人们希望从意外状况中“恢复”——提供可预测的失败结果,例如尝试付款时发生的 InsufficientFundsException。关于实际上需要怎样的“恢复”工作,当时的人们并不清楚。
运行时异常(runtime exception)也包含进了 Java 中。由于空指针、数据错误和非法状态/访问都可能在代码中的任何地方发生,因此将它们作为运行时异常的子类型。
运行时异常可以在任何地方抛出,而无需声明,并且更加方便。但是改用它们是否是正确的选择呢?
缺点
这里的关键在于运行时和检查异常在功能上是等效的。没有什么处理或恢复是检查异常能做,而运行时异常做不了的。
反对“检查的”异常的最大论点是,大多数异常无法修复。一个简单的事实是,我们无法控制损坏的代码/子系统。我们看不到实现,对此不承担任何责任,也无法修复它。
尤其成问题的是 JDBC(SQLException)和 EJB 的 RMI(RemoteException)。这些带来了普遍存在的、系统性的、实际上无法修复的可靠性问题,并不会像原始的“受检查的异常”理念确定可修复的突发事件。
对于任何方法,它调用的所有子方法都会有失败的可能性。潜在的故障会累积在调用树中。在方法签名上声明它们已经无法为开发人员提供需要关注的重点位置了——因为声明的异常已经遍布整个调用树。
大多数 EJB 开发人员都经历过这种情况——通过层或整个代码库的方法都需要声明的异常。调用一个具有不同异常的方法需要调整成打方法。
许多开发人员被告知要捕获低级别的异常,然后将其重新抛出为更高级别(应用程序级别)的检查异常。这需要大量的(每个项目可多达 2000 个)非功能性的“catch-throw”代码块。
吞下异常、隐藏原因、重复记录以及返回“空”/未初始化的数据,这些都是很常见的情况。大多数项目可以统计出 600 多个错误的编码或完全错误。
最终,开发人员开始抵触大量的“catch”代码块,不想再看到它们成为错误的来源。
检查异常——与函数式编程不兼容
然后,我们来看看 Java 8,它具有新的函数式编程特性——例如 lambda、Stream 和函数组合。
这些特性建立在泛型的基础上——参数和返回类型已泛化,因此无论项目类型如何,我们都可以编写执行通用操作的迭代和流操作(forEach、map、flatMap)。
但与数据类型不同,声明的异常无法泛化。
在 Java 中,这样的流操作是不可行的(例如 Stream.map):它需要一个 lambda 来声明某些已检查的异常,并透明地将这个检查异常传递给周围的代码。
这一直是反对检查异常的主要论据——抛出和接收“catch”块之间介入的所有代码都必须意识到异常。
解决方法是将其“包装”在一个运行时异常中,从而隐藏异常的原始类型——这让原始概念中设想的特定于异常的“catch”块失去了用武之地。
最后,我们注意到 Java 8 中没有任何新的“函数式接口”声明检查异常,因此可以说 Java 的新理念已经和以前不同了。
结论
与之前的语言相比,Java 异常在可靠性和错误处理方面提供了重要优势。Java 提供了可靠的服务器和商业软件,使用的是 C/C++永远无法做到的方式。
检查异常从其出发点来看,是处理“突发事件”而不是“失败”的尝试。其明确目标是高亮显示特定的可预测点(无法连接、找不到文件等)并确保开发人员能够处理这些点。
最初的概念从未包含在内的是,要强制声明发生了大量系统性和不可恢复的故障。这些失败永远都不是正确的,不能被声明为检查异常。
通常,代码中可能会发生故障,而 EJB、Web 和 Swing/AWT 容器已经通过提供最外部的“失败请求”异常处理程序来解决这个问题。最基本的正确策略是回滚事务并返回错误。
运行时异常允许对检查异常进行任何异常处理,但避免了限制性的编码约束。这简化了代码,并使其更容易遵循尽早抛出、较晚捕获,在最外层/最高级别处理异常的最佳实践。
主流 Java 框架和影响者现在已经明确地摆脱了检查异常。Spring、Hibernate 和现代 Java 框架/供应商仅使用运行时异常,而这种便利性是它们流行的主要因素。
像是 Josh Bloch(Java Collections 框架)、Rod Johnson、Anders Hejlsberg(C#之父)、Gavin King 和 Stephen Colebourn(JodaTime)等大牛都反对检查异常。
现在,在 Java 8 中,lambda 是向前迈出的关键一步。这些语言特性从内部的函数式操作中抽象出“控制流”。如我们所见,这使得检查异常和“立即声明或处理”的要求过时了。
对于开发人员而言,关注可靠性并诊断可能的故障点(突发事件)(例如文件打开、数据库连接等)一直都是很重要的事情。如果我们在此时提供良好的错误消息,我们就能创建自诊断软件——这会是工程成就的巅峰之作。
但我们应该使用未检查的异常来做到这一点,并且如果必须重新抛出,则应始终使用运行时异常或特定于应用程序的子类。
正如 Stephen Colebourn 所说,如果你的项目仍在使用或提倡检查异常,则你的技能已过时 5-10 年了。Java 已经前进了很长的路程。
原文链接:
http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake
评论 3 条评论