在捕获已检查异常后重新抛出运行时异常

19

在Java中观察到一种惯例,即在处理Checked Exception后立即重新抛出RuntimeException

这种方式有好处也有坏处。当编译器强制通过Checked Exception处理某些内容时,开发人员可以通过捕获并将其重新抛出为RuntimeException来摆脱它。

请问是否有人能解释这种情况是否可以被视为良好的实践?如果是这样,这种方法是否会减少错误或使代码库不稳定?


4
C#不支持checked异常,因此完全可以只使用运行时异常创建稳定的代码库。这可能更多取决于开发人员的纪律性,而非其他任何因素。 - Henrik Aasted Sørensen
5个回答

30
实际上,无能力地尝试处理已检查异常会导致代码库不稳定。通常会出现以下情况:

实际上,是对处理已检查异常的无能力尝试导致了代码库的不稳定性。通常情况下,您会看到这种情况:

try {
   //stuff
} catch (IOException e) {
   log.error("Failed to do stuff", e);
   throw e;
}

然后,你将不得不再次处理它,通常会记录所有内容并使日志文件混乱。如果您不重新抛出异常,情况将变得更糟

try {
   // do stuff
} catch (IOException e) {
  return null;
}

现在调用代码不知道出了什么问题,更别提具体是什么了。与那些尝试相比,这实际上正是应用逻辑所需的:

try {
  // do stuff
} catch (IOException e) {
  throw new RuntimeException(e);
}

现在异常可以自由传播到调用栈,直到它到达定义良好的异常边界,在此处:

  1. 中止当前工作单元;
  2. 在单个、统一的位置记录日志。

简而言之,要决定是捕获和处理还是捕获和重新抛出,只需问自己这个问题:

这种异常的发生是否必须中止当前工作单元?

  • 如果:重新抛出未经检查的异常;
  • 如果不是:在catch块中提供有意义的恢复代码。(不,记录日志不是恢复)。

凭借多年的实际经验,我可以告诉您,超过90%的所有可能的已检查异常都属于“中止”类型,在发生的地方不需要处理。

反对已检查异常语言特性的论点

今天,已检查异常被广泛认为是语言设计上的失败实验,以下是其关键论点:

API创建者无权决定其异常在客户端代码中的语义。

Java的论据是异常可分为:

  1. 由编程错误引起的异常(未经检查);
  2. 由程序员无法控制的情况引起的异常(已检查)。

尽管这种划分在一定程度上是真实存在的,但只能从客户端代码的角度定义。更重要的是,在实践中这种划分并不是非常相关:真正重要的是什么时候必须处理异常。如果要在异常边界处晚期处理它,那么检查异常并没有任何好处。如果要早期处理,那么只有在某些情况下才有轻微的检查异常收益。

实践证明,检查异常提供的任何收益都被现实生活中对真实项目造成的损害所淹没,每个Java专业人士都有所见。Eclipse和其他IDE也应该承担责任,它们建议经验不足的开发人员将代码包装在try-catch中,然后想知道在catch块中写什么。

每当您遇到一个抛出异常的方法时,您就会发现另一个检查异常缺陷的活生证明。


1
在我看来,这是非常错误的,不应该被接受。如果你不想记录并重新抛出已检查异常,那么只需声明异常(throws CheckedException),否则对此不做任何处理,让调用者处理或以相同方式声明它。 - devconsole
7
如果遵循您的建议,则所有接口方法都会声明“throws Exception”。不可预测底层实现可能抛出什么异常。就语义而言,“throws Exception”与没有“throws”子句完全相同。 - Marko Topolnik
3
要将已检查异常传播到调用栈上,意味着你需要在所有函数中声明一个 "throws",这很快会使你的代码混乱。一种更清晰的方法是将其转换为未检查异常,并在你感兴趣的地方捕获它。请参阅鲍勃·马丁的书(《代码整洁之道》 - http://www.amazon.co.uk/dp/0132350882/?tag=hydra0b-21&hvadid=9550951749&ref=asc_df_0132350882)中讨论此问题的部分。 - Dave Richardson
1
@Pablo 我的观点是,受检异常促进编码错误,正如全世界的Java专业人员所见。我绝对不会声称它们强制产生错误;然而,具有讽刺意味的是,一个特别引入来保护经验不足的程序员免于错误的功能,却产生了完全相反的效果。 - Marko Topolnik
1
@devconsole 客户端不可能知道它必须要期望哪些异常,因为它总是可以得到任何RuntimeExceptionError。这个事实是无法避免的。 - Marko Topolnik
显示剩余17条评论

3
< p >"已检查异常" 的概念只存在于 Java 中,据我所知,Java 后没有任何一种语言采用了这种想法。

有太多的已检查异常被捕获...并静默忽略。

如果你看看 Scala,他们也放弃了它 - 它只是为了 Java 兼容性而存在。

在 Oracle 网站上的这个教程中,你会找到这个定义:

如果客户端可以合理地预期从异常中恢复,则将其作为已检查的异常。
如果客户端无法对异常进行任何恢复操作,则将其作为未经检查的异常。

这个概念在 Scala 中也被采用,而且很好用。

从技术上讲,你的提议是行得通的。无论哪种方式都需要纪律和代码审查。


“可以知道客户端何时“可以合理地预期从异常中恢复”这个想法已被证实是一种幻觉。为什么客户端不能从NPE中恢复呢?就我个人而言,我根本没有想到过这个想法。” - Marko Topolnik
不,我从来没有从NPE中恢复过来。如果NPE是您“预期”的内容,则始终可以在此之前进行检查-无需具有堆栈跟踪的昂贵异常,只需进行比较即可。如果它是意外的(编程错误,API更改),那么您该如何修复呢? - Beryllium
如果这种情况发生在三个API级别之下怎么办?你真的从未遇到过因为一个你甚至不知道是否为空的对象的私有属性导致的NPE吗?至少你必须承认这是一个可以想象的场景。例如,你可能需要解决一个你无法控制的代码缺陷。 - Marko Topolnik
不,那从来不会发生 :-) 我总是使用Options(例如Scala的Option)来确定属性是否可以为空。任何其他属性都绝不能为null。Options强制在之前检查内容,因此编译器拒绝编译源代码。第三方库会得到适配器或使用aspectj进行修复。 - Beryllium
好的,那么就你个人而言,即使你只需要一个单一的NPE解决方法,你也会引入另一个依赖项并显著提高项目的总复杂度。以这种态度来看,你并不代表更广泛的Java人口;但即使你是,你仍然不能否认这样的事情是可以想象的。检查异常的发明者希望我们相信这是不可想象的。 - Marko Topolnik
我们可以好好讨论这个问题,即使我们意见不同也没关系 :-) - Beryllium

2
术语“可以摆脱它”在这种情况下并不完全正确。这是消除异常的过程。
 try {

 } catch (Exception e){
     e.printStacktrace();
 } 

这是在try-catch中最常见的不良实践。你捕获了异常,然后只是打印它。在这种情况下,catch块捕获异常并仅打印它,而程序在catch块之后继续执行,就好像什么都没有发生一样。
当你决定捕获块而不是抛出异常时,必须能够处理异常。有时异常是不可管理的,必须抛出。
这是你应该记住的事情:
如果客户端可以采取某些替代操作来从异常中恢复,请将其作为已检查异常。如果客户端无法执行任何有用的操作,则使异常未经检查。有用的意思是采取步骤从异常中恢复,而不仅仅是记录异常。
如果你不打算做一些有用的事情,那么就不要捕获异常。重新抛出它作为运行时异常是有原因的:如前所述,程序不能继续进行,就好像什么都没有发生一样。因此,一个好的做法是:
try {

} catch (Exception e){
    //try to do something useful  
    throw new RuntimeException(e); 
}

这意味着:您刚捕获了一个异常(例如SQLException),在没有停止和重置线程的情况下无法恢复。您捕获它,尝试在其中进行一些操作(如重置某些内容、关闭打开的套接字等),然后抛出一个RuntimeException()。 < p > RuntimeException将挂起整个线程,避免程序继续执行,就好像什么都没有发生。此外,您能够管理其他异常而不仅仅是打印它们。

0

根据上下文,这可能是可以的,也可能不可以,但可能性不大。

概括而言,RuntimeExceptions 应仅用于指示编程错误(例如 IllegalArgumentException 和 IllegalStateException)。它们不一定是可检查的异常,因为您通常假设您的程序正确无误,直到证明存在问题,且您无法以有意义的方式处理这些异常(必须发布更新版本的程序)。

另一个有效使用运行时异常的方法是在使用会捕获和处理异常的框架时。在这种情况下,如果您不打算处理这个异常,那么在每个方法中声明异常只会变得繁琐。

因此,总体而言,我认为将已检查异常重新抛出为运行时异常是非常不好的做法,除非您有一个能够正确处理它的框架。


很抱歉,但是一个异常是“编程错误”还是“致命故障”,这与代码有什么关系呢?而且,这又怎么可能由包含错误的程序本身来决定呢?开发人员所需要的只是日志中的堆栈跟踪,以便找出异常的根本原因。FileNotFoundException 既可以是编程错误,也可以是不幸的运行时情况。 - Marko Topolnik
其次,每个编写良好的程序都必须有一个异常屏障。无论是在框架中还是在您自己的代码中,都没有关系。它是处理所有中止异常的屏障:记录它们,清理失败请求后继续执行。 - Marko Topolnik

0
一般规则是:当调用者被告知异常信息后可能能够进行某种恢复时,抛出已检查异常。否则,抛出未检查异常。
这个规则适用于首次抛出异常的情况。
但是,在捕获异常并想知道是否要抛出已检查或未检查异常时,也适用此规则。因此,在捕获已检查异常后抛出 RunTimeException 并没有约定俗成。这是根据具体情况而定的。
一个小提示:如果你只是在捕获异常后重新抛出已检查异常而不做其他操作,大多数情况下最好不要捕获异常,而是将其添加到方法抛出的异常中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接