多年来,我一直无法得到以下问题的一个像样的答案:为什么一些开发人员如此反对受控异常?我有过无数次的对话,在博客上读过一些东西,读过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,而不是将抛出添加到他们的方法中。我想了解不喜欢受控异常的动机。
好吧,这不是关于显示堆栈跟踪或无声崩溃。它是关于在层与层之间沟通错误的能力。
The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding throws declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throws IOException, it's easy to overlook this).
实际上,我现在在一个应用程序中处理这种情况。我们将异常重新打包为AppSpecificException。这使得签名非常干净,我们不必担心在签名中爆炸。
当然,现在我们需要在更高的级别上专门化错误处理,实现重试逻辑等等。所有的东西都是AppSpecificException,所以我们不能说“如果一个IOException被抛出,重试”或“如果ClassNotFound被抛出,完全中止”。我们没有可靠的方法来获得真正的异常,因为当它们在我们的代码和第三方代码之间传递时,东西会一次又一次地重新打包。
这就是为什么我非常喜欢python中的异常处理。你只能捕获你想要和/或能处理的东西。其他所有东西都冒出来了,就好像你自己重新扔了它一样(不管怎样你已经这么做了)。
我一次又一次地发现,在我提到的整个项目中,异常处理分为3类:
Catch and handle a specific exception. This is to implement retry logic, for example.
Catch and rethrow other exceptions. All that happens here is usually logging, and its usually a trite message like "Unable to open $filename". These are errors you can't do anything about; only a higher levels knows enough to handle it.
Catch everything and display an error message. This is usually at the very root of a dispatcher, and all it does it make sure it can communicate the error to the caller via a non-Exception mechanism (popup dialogue, marshaling an RPC Error object, etc).
异常类
当谈到异常时,我总是会参考Eric Lippert的恼人异常博客文章。他将例外情况分为以下几类:
Fatal - These exceptions are not your fault: you cannot prevent then, and you cannot sensibly handle them. For example, OutOfMemoryError or ThreadAbortException.
Boneheaded - These exceptions are your fault: you should have prevented them, and they represent bugs in your code. For example, ArrayIndexOutOfBoundsException, NullPointerException or any IllegalArgumentException.
Vexing - These exceptions are not exceptional, not your fault, you cannot prevent them, but you'll have to deal with them. They are often the result of an unfortunate design decision, such as throwing NumberFormatException from Integer.parseInt instead of providing an Integer.tryParseInt method that returns a boolean false on parse failure.
Exogenous - These exceptions are usually exceptional, not your fault, you cannot (reasonably) prevent them, but you must handle them. For example, FileNotFoundException.
API用户:
不能处理致命或愚蠢的异常。
应该处理令人烦恼的异常,但它们不应该出现在理想的API中。
必须处理外生异常。
已检查的异常
API用户必须处理特定的异常,这是调用方和被调用方之间的方法契约的一部分。契约指定了被调用方期望的参数的数量和类型,调用方期望的返回值类型,以及调用方期望处理的异常。
由于API中不应该存在令人烦恼的异常,因此只有这些外生异常必须作为方法契约的一部分进行检查。相对较少的异常是外生的,因此任何API都应该有相对较少的受控异常。
受控异常是必须处理的异常。处理异常就像吞下它一样简单。在那里!异常被处理。时期。如果开发者想这样处理,没问题。但他不能忽视这个例外,已经得到了警告。
API的问题
但是任何检查了令人烦恼和致命异常的API(例如JCL)都会给API用户带来不必要的压力。必须处理这样的异常,但是要么异常太常见,以至于一开始就不应该是异常,要么在处理它时什么都做不了。这导致Java开发人员讨厌受控异常。
此外,许多api没有适当的异常类层次结构,导致各种非外生异常原因都由单个受控异常类表示(例如IOException)。这也导致Java开发人员讨厌受控异常。
结论
外生异常指的是那些不是您的错、无法预防、并且应该处理的异常。这些构成了所有可能引发的异常的一个小子集。api应该只检查外生异常,所有其他异常都不检查。这将使API更好,给API用户带来更少的压力,从而减少捕获所有、吞咽或重新抛出未经检查的异常的需要。
所以不要讨厌Java和它的受控异常。相反,讨厌那些过度使用受控异常的api。