Checked Exception | Java语言设计者的失误?

2023-03-18 11:40:26 浏览数 (2)

背景

最近公司一直在执行sonar扫描代码bug、漏洞及异味,但发现了很对异常处理的问题,大多数是对Java异常处理不正确导致的,那本文就谈谈Java的异常是什么?设计者的初衷又是什么?

Exception 介绍

ExceptionError都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。

ExceptionError体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。

Error是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如虚拟机自身)处于非正常的、不可恢复状态。既然是不正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。

Exception异常本身又分为可检查(checkd)异常和不可检查(uncheckd)异常。

可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不可检查的ErrorThrowable,而不是Exception,通常我在编码过程中编译器会提示如何处理异常,类似于我们常见的try catch或者继续throw

不检查异常就是所谓的运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码过程中避免的代码逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求捕获此类异常。

争议点

Checkd Exception(可检查异常)一直都是Java语言比较有争议的一个功能。

Java可检查异常的提倡者认为通过检查异常能够确保它们从异常中恢复;而反对者却认为因为这些错误都是常见的错误,所有它们根本无法从异常中恢复。

同时,Java8lambdas已经问世一段时间。在它们的世界中Checkd Exception是如何使用的呢?

设计者意图

90年代中期,Sun公司的James Gosling提出了一种新的语言(Java)Java这门编程语言本身是一门面向服务端长期运行的编程语言,借鉴并弥补了C、C 的不足,当然异常处理也不例外。

C语言的异常处理机制,因为C本身是单返回值,异常信息通常通过一个int值来表示成功还是失败

C 弥补了C的不足,出现异常时可以发送错误信号,即引入了Exception机制,出现异常、抛出异常。但C 同时带来了另外一个问题,调用的任何一个函数都可能出现异常,即异常信息不确定。

Java设计者吸取了C 异常设计的经验教训,他认为必须有更好的方法,并将异常的概念引入到Java中。并认为异常本身并不重要,而在于发生了什么异常。所以Java引入了Checkd ExceptionJava方法的所有者声明异常信息,方法调用者处理异常信息,这使得ExceptionJava中变成了司空见惯的事情。所以就导致了代码中经常出现catch(e Exception){//忽略},直接捕获并忽略异常信息,并不能使异常信息有效传递。

检查异常的目的是在本地标记并迫使开发人员处理可能的异常。已检查的异常必须在方法签名上声明或处理。

这是为了鼓励软件的可靠性和弹性。旨在从意外情况中恢复 – 除了成功以外的可预测结果,例如尝试付款时出现InsufficientFundsException。关于实际上需要进行什么恢复,目前尚没有明确答案。

运行时异常也包含在Java中。由于空指针、数据错误、非法状态、访问都可能在代码中的任何地方发生,因此将它们作为RuntimeException的子类。这种异常也就是类似于C 不可检查异常。

运行时异常可以在任何地方抛出,而无需声明,并且更加方便。但是直接使用它们是否正确?

优缺点

这里的关键点是运行时和检查异常在功能上是等效的。但已检查异常可以执行的处理或恢复,而运行时异常则无法做到。

反对检查异常的最大论点是,大多数异常无法修复。一个简单的事实是,我们的子系统都是正常的,我们看不到具体实现逻辑,我们对此不负责,也无法修复其中的异常,所以不要往上层抛出可检查异常。

尤其问题是JDBC(SQLException)EJB RMI(RemoteException)。这些强迫性普遍存在的系统可靠性问题(实际上不是可修复的)不是按照原始的可检查异常概念来确定可修复的突发事件,而是要广泛声明。

对于任何方法,失败的可能性都包括它调用的所有子方法。潜在的故障会累积在调用链中。在方法签名上声明这些异常,并且不再为开发人员提供一个特定的和局部的返回值,让开发人员检查在调用链中传播的受检查异常。

大多数EJB开发人员都经历过这种情况–整个层或整个代码库的方法都需要声明异常。调用具有不同异常的方法需要调整许多方法。

许多开发人员被告知要捕获底层代码的异常,然后将它们重新抛出为更高级别(应用程序级别)的已检查异常。这需要一定的工作量(每个项目最多2000个)非功能性的抛雪球块。

于是Java开发人员吞下异常、隐藏原因、重复记录日志、返回null,未初始化的数据都变得很普遍。大多数项目因为异常问题可能会算出上百个错误编码或完全错误。

最终,开发人员对大量的catch块产生了反感,这些块本身已经成为错误的根源。

Checked Exception - 与功能代码不兼容

然后我们来看看Java8,它具有新的编程范式-例如lambda、Streams功能组合。

这些特性是建立在泛型之上的——参数和返回类型被泛化,这样迭代和流操作(forEach、map、flatMap)可以被编写来执行一个公共操作,而不考虑对象类型。

但是,与数据类型不同,声明的异常无法泛化。

Java中没有提供流操作(例如Stream.map)可检查异常,该操作需要一个lambda来声明某些已检查的异常,并透明地将相同的已检查的异常传递给周围的代码。

这一直是反对检查异常的主要要点–抛出和接收catch块之间的所有代码逻辑都必须意识到异常。

解决方法是在RuntimeException包装它,它隐藏了异常的原始类型,使得原始概念中设想的特定于异常的catch块变得毫无用处。

最后,我们可以简单地理解Java的新理念,注意到Java8中没有一个新的函数接口声明checked异常。

可检查异常使用中注意事项

  • 所有的方法尽量不要定义可检查异常,而是通过返回错误信息。
  • 尽量不要尝试捕获最顶级的Exception,尽量捕获具体的Exception,因为代码本身是写给人看的,机器只是顺便执行,我们应该尽量通过代码显示直观的信息,而不是只是Exception,因为Exception恰恰隐藏的真正的异常信息。
  • try/catch范围尽可能小,因为它本身需要创建堆栈信息,会产生额外的性能开销。所以只需要捕获需要的代码片段,尽量不要使用一个大的try包住整个代码块。
  • 不要生吞异常。这是异常处理过程中需要特别注意,因为它可能会使出现问题后难以诊断。有时我们的主要精力都放在了主要逻辑上面,往往对异常信息疏忽或者认为该异常不会出现,我们千万不要做这种假设,我们以为的不可能出现的细节问题,往往会无限放大。

例如下面片段,这个片段导致的问题在于没有异常输出也没有日志打印,更没有抛出来:

代码语言:javascript复制
try{
//业务逻辑
}catch(Exception e){
//没有任何逻辑
}

下面这段代码的问题在于直接标准输出异常,通过这种方式难以判断该日志如何和出现问题的逻辑结合起来,导致难以诊断问题所在,正确的姿势应该详细把错误信息输出到日志中。

代码语言:javascript复制
try{
//业务逻辑
}catch(Exception e){
    e.printStackTrace()
}

结论

与以前的语言相比,Java异常在可靠性和错误处理方面提供了主要优势。Java支持可靠的服务器和商业软件,这是C/C 无法做到的。

可检查异常以其原始形式是试图处理突发事件而不是失败。值得称赞的目标是突出显示特定的可预测点(无法连接、找不到文件等)并确保开发人员能够处理这些点。

Java异常最初的概念中从未包括的是,大量系统性和不可恢复的故障。这些失败从未被声明为受检查异常,这也就导致Java倡导者认为Java可检查异常出现问题,根本原因在于开发者的使用方式存在问题。

通常,代码中可能会发生故障,而EJB、Web、Swing/AWT容器已经通过提供最外部的失败请求异常处理程序来解决此问题。最基本的正确策略是回滚事务并返回错误。

运行时异常允许对捕获的异常进行任何可能的异常处理,但要避免限制性的编码。使用Java异常过程中要遵循早期抛出、延迟捕获(最外层)的最佳实践,通过这些可以简化编码。

一些领先的和有影响力的Java框架现在已经明确地摆脱了检查异常。Spring、Hibernate和现代Java框架/供应商仅使用运行时异常,而这种便利性是它们流行的主要因素。

诸如Josh BlochJava Collections框架)、Rod JohnsonAnders HejlsbergC#之父)、Gavin KingStephen ColebournJodaTime)等人都反对检查异常。

现在,在Java8中,lambda是向前迈出的基本一步。这些语言特性将控制流从内部的功能操作中抽象出来。正如我们所看到的,这使得检查异常成为过去,即立即声明或处理的要求。

对于开发人员而言,始终必须注意可靠性并诊断可能的故障点(突发事件),例如打开文件、数据库连接等,这一点始终很重要。如果此时提供了良好的错误消息,我们将创建自诊断软件–工程成就的巅峰之作。

但是,我们应该使用未经检查的异常来执行此操作,并且如果必须重新抛出,则应始终使用RuntimeException或特定于应用程序的子类。

正如史蒂芬·科尔本(Stephen Colebourn)所说,如果您的项目仍在使用或提倡检查异常,则您的技能已过期5-10年。Java本身已经在前进了。

最后一点对于Java的可检查异常也不必要矫枉过正,因为Java的可检查异常已经遍布于大大小小的各种组件和系统中,对于一些分布式系统,比如出现网络等问题时,确实可以通过异常信息进行恢复,通过这种方式使我们可以构建出高质量的软件系统。

参考资料

http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

https://www.oracle.com/technical-resources/articles/enterprise-architecture/effective-exceptions-part1.html

https://testing.googleblog.com/2009/09/checked-exceptions-i-love-you-but-you.html

0 人点赞