背景
最近公司一直在执行sonar
扫描代码bug
、漏洞及异味,但发现了很对异常处理的问题,大多数是对Java
异常处理不正确导致的,那本文就谈谈Java
的异常是什么?设计者的初衷又是什么?
Exception 介绍
Exception
和Error
都是继承了Throwable
类,在Java
中只有Throwable
类型的实例才可以被抛出(throw
)或者捕获(catch
),它是异常处理机制的基本组成类型。
Exception
和Error
体现了Java
平台设计者对不同异常情况的分类。Exception
是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error
是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如虚拟机自身)处于非正常的、不可恢复状态。既然是不正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError
之类,都是Error
的子类。
Exception
异常本身又分为可检查(checkd
)异常和不可检查(uncheckd
)异常。
可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不可检查的Error
是Throwable
,而不是Exception
,通常我在编码过程中编译器会提示如何处理异常,类似于我们常见的try catch
或者继续throw
。
不检查异常就是所谓的运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException
之类,通常是可以编码过程中避免的代码逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求捕获此类异常。
争议点
Checkd Exception
(可检查异常)一直都是Java
语言比较有争议的一个功能。
但Java
可检查异常的提倡者认为通过检查异常能够确保它们从异常中恢复;而反对者却认为因为这些错误都是常见的错误,所有它们根本无法从异常中恢复。
同时,Java8
和lambdas
已经问世一段时间。在它们的世界中Checkd Exception
是如何使用的呢?
设计者意图
90
年代中期,Sun
公司的James Gosling
提出了一种新的语言(Java)
。Java
这门编程语言本身是一门面向服务端长期运行的编程语言,借鉴并弥补了C、C
的不足,当然异常处理也不例外。
C
语言的异常处理机制,因为C
本身是单返回值,异常信息通常通过一个int
值来表示成功还是失败
C
弥补了C
的不足,出现异常时可以发送错误信号,即引入了Exception
机制,出现异常、抛出异常。但C
同时带来了另外一个问题,调用的任何一个函数都可能出现异常,即异常信息不确定。
Java
设计者吸取了C
异常设计的经验教训,他认为必须有更好的方法,并将异常
的概念引入到Java
中。并认为异常本身并不重要,而在于发生了什么异常。所以Java
引入了Checkd Exception
;Java
方法的所有者声明异常信息,方法调用者处理异常信息,这使得Exception
在Java
中变成了司空见惯的事情。所以就导致了代码中经常出现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 Bloch
(Java Collections
框架)、Rod Johnson
、Anders Hejlsberg
(C#
之父)、Gavin King
和Stephen Colebourn
(JodaTime
)等人都反对检查异常。
现在,在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