多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过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,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。


当前回答

在过去的三年中,我一直在与一些开发人员一起开发相对复杂的应用程序。我们有一个代码库,它经常使用检查异常,并进行适当的错误处理,而其他一些代码库则没有。

So far, I have it found easier to work with the code base with Checked Exceptions. When I am using someone else's API, it is nice that I can see exactly what kind of error conditions I can expect when I call the code and handle them properly, either by logging, displaying or ignoring (Yes, there is valid cases for ignoring exceptions, such as a ClassLoader implementation). That gives the code I am writing an opportunity to recover. All runtime exceptions I propagate up until they are cached and handled with some generic error handling code. When I find a checked exception that I don't really want to handle at a specific level, or that I consider a programming logic error, then I wrap it into a RuntimeException and let it bubble up. Never, ever swallow an exception without a good reason (and good reasons for doing this are rather scarce)

当我使用没有检查异常的代码库时,它使我在调用函数时很难预先知道我期望什么,这可能会严重破坏一些东西。

当然,这完全取决于开发者的偏好和技能。编程和错误处理的两种方式可能同样有效(或无效),所以我不会说只有一种方法。

总而言之,我发现使用受控异常更容易,特别是在有很多开发人员的大型项目中。

其他回答

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

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

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

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

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

我认为这是一个很好的问题,没有任何争论。我认为第三方库(通常)应该抛出未经检查的异常。这意味着你可以隔离你对库的依赖(也就是说,你既不用重新抛出它们的异常,也不用抛出异常——这通常是不好的做法)。Spring的DAO层就是一个很好的例子。

On the other hand, exceptions from the core Java API should in general be checked if they could ever be handled. Take FileNotFoundException or (my favourite) InterruptedException. These conditions should almost always be handled specifically (i.e. your reaction to an InterruptedException is not the same as your reaction to an IllegalArgumentException). The fact that your exceptions are checked forces developers to think about whether a condition is handle-able or not. (That said, I've rarely seen InterruptedException handled properly!)

还有一件事——RuntimeException并不总是“开发人员出错的地方”。当您尝试使用valueOf创建枚举且没有该名称的枚举时,将抛出非法参数异常。这并不一定是开发者的错误!

受控异常的一个问题是,如果一个接口的一个实现使用了它,异常通常会附加到该接口的方法上。

受控异常的另一个问题是它们容易被滥用。java.sql就是一个完美的例子。连接的close()方法。它可以抛出一个SQLException,即使您已经显式地声明您已经完成了连接。close()可能传达了哪些您所关心的信息?

通常,当我关闭()一个连接*时,它看起来像这样:

try {
    conn.close();
} catch (SQLException ex) {
    // Do nothing
}

另外,不要让我开始各种解析方法和NumberFormatException... .NET的TryParse,它不抛出异常,使用起来要容易得多,以至于不得不回到Java(我工作的地方同时使用Java和c#)。

*作为额外的注释,PooledConnection的connection .close()甚至不关闭连接,但您仍然必须捕获SQLException,因为它是一个已检查异常。

我不想重复所有(许多)反对受控异常的原因,我只选一个。我已经记不清写了多少次这段代码了:

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

99%的情况下我对此无能为力。最后,块进行必要的清理(或者至少它们应该这样做)。

我也记不清有多少次看到这样的场景:

try {
  // do stuff
} catch (AnnoyingCheckedException e) {
  // do nothing
}

为什么?因为有人要处理这件事,而且很懒。错了吗?确定。会发生吗?绝对的。如果这是一个未检查的异常呢?应用程序就会死掉(这比吞下一个异常更好)。

然后我们有一些令人愤怒的代码,它使用异常作为一种流控制形式,就像java.text.Format所做的那样。Bzzzt。错了。用户在表单的数字字段中输入“abc”也不例外。

好吧,我想这是三个原因。

《有效的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