多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过Bruce Eckel说的话(我看到的第一个站出来反对他们的人)。

我目前正在编写一些新代码,并非常注意如何处理异常。我试图了解那些“我们不喜欢受控异常”的人的观点,但我仍然看不出来。

我的每一次谈话都以同样的问题结束。让我把它建立起来:

一般来说(从Java的设计方式来看),

Error is for things that should never be caught (VM has a peanut allergy and someone dropped a jar of peanuts on it) RuntimeException is for things that the programmer did wrong (programmer walked off the end of an array) Exception (except RuntimeException) is for things that are out of the programmer's control (disk fills up while writing to the file system, file handle limit for the process has been reached and you cannot open any more files) Throwable is simply the parent of all of the exception types.

我听到的一个常见的说法是,如果发生了异常,那么所有开发人员要做的就是退出程序。

我听到的另一个常见论点是受控异常使得重构代码更加困难。

对于“我要做的就是退出”的参数,我说即使你退出了,你也需要显示一个合理的错误消息。如果你只是在处理错误上押注,那么当程序在没有明确说明原因的情况下退出时,你的用户不会太高兴。

对于“它使重构变得困难”的人群来说,这表明没有选择适当的抽象级别。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。

我对用catch(Exception)(或在某些情况下用catch(Throwable)包装Main没有问题,以确保程序可以优雅地退出-但我总是捕获我需要的特定异常。这样做至少可以显示适当的错误消息。

人们从来不会回答的问题是:

如果抛出RuntimeException 子类代替异常 子类,你怎么知道 你应该去抓?

如果答案是catch Exception,那么您也在以与系统异常相同的方式处理程序员错误。在我看来这是不对的。

如果你捕获Throwable,那么你是在以同样的方式处理系统异常和虚拟机错误(等等)。在我看来这是不对的。

如果答案是您只捕获您知道抛出的异常,那么您如何知道抛出了哪些异常呢?当程序员X抛出一个新的异常而忘记捕获它时会发生什么?这对我来说似乎很危险。

我认为显示堆栈跟踪的程序是错误的。不喜欢受控异常的人不会有这种感觉吗?

所以,如果你不喜欢受控异常,你能解释一下为什么不,并回答没有得到回答的问题吗?

我不是在寻求什么时候使用这两种模型的建议,我想知道的是为什么人们从RuntimeException扩展,因为他们不喜欢从Exception扩展,和/或为什么他们捕获一个异常然后重新抛出一个RuntimeException,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。


当前回答

《有效的Java异常》一文很好地解释了什么时候使用未检查的异常,什么时候使用已检查的异常。以下是那篇文章中的一些引语,以突出其要点:

应急预案: 一种期望的条件,要求方法提供可根据该方法的预期目的来表达的替代响应。方法的调用者期望这些类型的条件,并有一个策略来处理它们。 错: 一种计划外的情况,阻止方法实现其预期目的,如果不参考方法的内部实现,则无法描述该情况。

(SO不支持表格,所以你可能想从原始页面阅读以下内容…)

Contingency Is considered to be: A part of the design Is expected to happen: Regularly but rarely Who cares about it: The upstream code that invokes the method Examples: Alternative return modes Best Mapping: A checked exception Fault Is considered to be: A nasty surprise Is expected to happen: Never Who cares about it: The people who need to fix the problem Examples: Programming bugs, hardware malfunctions, configuration mistakes, missing files, unavailable servers Best Mapping: An unchecked exception

其他回答

简而言之:

异常是一个API设计问题。——不多不少。

检查异常的参数:

为了理解受控异常为什么不是好事,让我们把问题转过来问:受控异常什么时候或为什么有吸引力,也就是说,为什么你希望编译器强制声明异常?

答案是显而易见的:有时您需要捕获异常,而这只有在被调用的代码为您感兴趣的错误提供了特定的异常类时才有可能。

因此,受控异常的理由是,编译器强迫程序员声明抛出了哪些异常,希望程序员随后也会记录特定的异常类和导致异常的错误。

但在现实中,往往是一个包装。acme只抛出AcmeException而不抛出特定的子类。然后调用者需要处理、声明或重新发出acmeexception信号,但仍然不能确定是发生了AcmeFileNotFoundError还是发生了AcmePermissionDeniedError。

因此,如果您只对AcmeFileNotFoundError感兴趣,解决方案是向ACME程序员提交一个特性请求,并告诉他们实现、声明和记录AcmeException的子类。

所以为什么要麻烦呢?

因此,即使使用受控异常,编译器也不能强迫程序员抛出有用的异常。这仍然只是API质量的问题。

因此,没有受控异常的语言通常情况不会更糟。程序员可能倾向于抛出一般Error类的非特定实例,而不是AcmeException,但如果他们完全关心API质量,他们终究会学会引入AcmeFileNotFoundError。

总的来说,异常的规范和文档与普通方法的规范和文档没有太大的区别。这些也是一个API设计问题,如果程序员忘记实现或导出一个有用的特性,那么API就需要改进,以便您可以有效地使用它。

如果遵循这条推理线,很明显,在Java等语言中非常常见的声明、捕获和重新抛出异常的“麻烦”通常没有什么价值。

同样值得注意的是,Java VM没有检查过的异常——只有Java编译器检查它们,并且在运行时,类文件的异常声明更改后是兼容的。Java虚拟机的安全性并不是通过受控异常来提高的,而是通过编码风格来提高的。

下面是反对受控异常的一个论点(来自joelonsoftware.com):

The reasoning is that I consider exceptions to be no better than "goto's", considered harmful since the 1960s, in that they create an abrupt jump from one point of code to another. In fact they are significantly worse than goto's: They are invisible in the source code. Looking at a block of code, including functions which may or may not throw exceptions, there is no way to see which exceptions might be thrown and from where. This means that even careful code inspection doesn't reveal potential bugs. They create too many possible exit points for a function. To write correct code, you really have to think about every possible code path through your function. Every time you call a function that can raise an exception and don't catch it on the spot, you create opportunities for surprise bugs caused by functions that terminated abruptly, leaving data in an inconsistent state, or other code paths that you didn't think about.

这个问题

我所看到的异常处理机制最糟糕的问题是它引入了大规模的代码复制!让我们诚实地说:在大多数项目中,在95%的时间里,开发人员真正需要做的就是以某种方式与用户沟通(在某些情况下,也需要与开发团队沟通,例如通过发送带有堆栈跟踪的电子邮件)。因此,通常在处理异常的每个地方都使用相同的代码行/块。

让我们假设我们在每个catch块中对某种类型的检查异常做简单的日志记录:

try{
   methodDeclaringCheckedException();
}catch(CheckedException e){
   logger.error(e);
}

如果这是一个常见的异常,那么在更大的代码库中甚至可能有数百个这样的try-catch块。现在让我们假设我们需要引入基于弹出对话框的异常处理,而不是控制台日志记录,或者开始额外向开发团队发送电子邮件。

等一下……我们真的要在代码中编辑这几百个位置吗?你明白我的意思:-)。

解决方案

为了解决这个问题,我们引入了异常处理程序的概念(我将进一步将其称为EH)来集中异常处理。对于每个需要处理异常的类,依赖注入框架都会注入一个异常处理程序实例。所以异常处理的典型模式现在看起来像这样:

try{
    methodDeclaringCheckedException();
}catch(CheckedException e){
    exceptionHandler.handleError(e);
}

现在要定制我们的异常处理,我们只需要更改一个地方的代码(EH代码)。

当然,对于更复杂的情况,我们可以实现eh的几个子类,并利用DI框架提供给我们的特性。通过改变我们的DI框架配置,我们可以轻松地在全局切换EH实现,或者为有特殊异常处理需求的类提供特定的EH实现(例如使用Guice @Named注释)。

这样我们就可以区分应用程序的开发版本和发布版本中的异常处理行为。开发——记录错误并停止应用程序,用更详细的信息刺激记录错误并让应用程序继续执行)。

最后一点

最后但并非最不重要的是,似乎可以通过将异常“向上传递”直到它们到达某个顶级异常处理类来获得同样的集中化。但这会导致代码和方法签名的混乱,并引入本线程中其他人提到的维护问题。

受控异常最初的形式是试图处理意外事件,而不是失败。值得称赞的目标是突出特定的可预测点(无法连接,文件未找到等)并确保开发者能够处理这些问题。

最初的概念中从未包括的是强制宣布大量系统性和不可恢复的故障。这些失败不能被声明为受控异常。

在代码中失败通常是可能的,EJB、web和Swing/AWT容器已经通过提供最外层的“失败请求”异常处理程序来满足这一点。最基本的正确策略是回滚事务并返回错误。

关键的一点是,运行时异常和受控异常在功能上是等价的。检查异常所能做的处理或恢复是运行时异常所不能做的。

反对“受控”异常的最大争论是大多数异常无法修复。简单的事实是,我们并不拥有发生故障的代码/子系统。我们看不到实现,我们不负责它,也不能修复它。

如果我们的应用程序不是一个数据库..我们不应该试图修复DB。这违反了封装原则。

特别有问题的是JDBC (SQLException)和用于EJB的RMI (RemoteException)。而不是根据最初的“受控异常”概念确定可修复的偶发事件,这些强制普遍存在的系统可靠性问题,实际上是不可修复的,被广泛宣布。

Java设计中的另一个严重缺陷是异常处理应该正确地放在尽可能高的“业务”或“请求”级别。这里的原则是“早扔晚接”。受控异常做的很少,但会阻碍这一点。

我们在Java中有一个明显的问题,即需要数千个不做任何事情的try-catch块,其中很大一部分(40%以上)被错误编码。这些方法几乎都没有实现任何真正的处理或可靠性,但会增加大量的编码开销。

最后,“受控异常”与FP函数式编程几乎不兼容。

他们对“立即处理”的坚持与“延迟捕获”异常处理最佳实践和任何抽象循环/或控制流的FP结构都不一致。

许多人谈论“处理”受控异常,但他们是在胡说八道。在失败后继续使用null、不完整或不正确的数据来假装成功没有处理任何事情。这是最低级的工程/可靠性渎职。

干净利落的失败,是处理异常最基本的正确策略。回滚事务、记录错误和向用户报告“失败”响应是良好的实践——最重要的是,防止不正确的业务数据提交到数据库。

在业务、子系统或请求级别上,异常处理的其他策略是“重试”、“重新连接”或“跳过”。所有这些都是通用的可靠性策略,并且在运行时异常时工作得更好。

最后,失败比使用不正确的数据运行要好得多。继续将导致次要错误,远离原始原因,更难调试;或者最终将导致提交错误的数据。有人会因此被解雇。

看到的: ——http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/

的确,受控异常一方面增加了程序的健壮性和正确性(你被迫对接口做出正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,你面临的问题是,由于异常“冒出来”,当你改变一个方法抛出的异常时,你经常需要改变大量的方法(所有的调用者,以及调用者的调用者,等等)。

Java中的受控异常不能解决后一个问题;c#和VB。我们把孩子和洗澡水一起倒掉。

OOPSLA 2005论文(或相关技术报告)中描述了一种折衷的好方法。

简而言之,它允许你说:方法g(x)像f(x)一样抛出,这意味着g抛出f抛出的所有异常。瞧,没有级联更改问题的受控异常。

虽然这是一篇学术论文,但我还是鼓励你阅读(部分)它,因为它很好地解释了受控异常的优点和缺点。