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


当前回答

没有人提到的一件重要的事情是它如何干扰接口和lambda表达式。

假设您定义了MyAppException扩展异常。它是由应用程序抛出的所有异常继承的顶级异常。在某些地方,您不想对特定的异常做出反应,您希望调用者解决它,因此您声明throws MyAppException。

一切看起来都很好,直到你想使用别人的界面。显然它们没有声明抛出MyAppException的意图,所以编译器甚至不允许你调用在那里声明抛出MyAppException的方法。这对于java.util.function来说尤其痛苦。

但是,如果您的异常扩展了RuntimeException,那么接口就不会有问题。如果愿意,可以在JavaDoc中提到异常。但除此之外,它只是无声地穿过任何东西。当然,这意味着它可以终止你的申请。但是在很多企业软件中都有异常处理层,未检查的异常可以省去很多麻烦。

其他回答

In my opinion the checked exceptions are a very fine concept. Unfortunately the most programmers who have worked together we me have another opinion so that the projects have a lot of wrong used exception handling. I have seen that the most programmers create one (only one) exception class, a subclass of RuntimeException. That contains a message, sometimes a multi language key. I have no chance to argument against this. I have the impression that I talk to a wall when I explain them what anti patterns are, what a contracts of a methods are... I'm a little bit disappointed.

但是今天,对所有事情都有一个通用运行时异常的概念显然是一种反模式。他们使用它来检查用户输入。该异常被抛出,以便用户对话框可以从中产生错误消息。但并不是每个方法的调用者都是对话!通过抛出运行时异常,方法的契约被更改但没有声明,因为它不是一个受控异常。

希望他们今天学到了一些东西,并将在另一个地方进行检查(这是有用和必要的)。只使用受控异常不能解决问题,但受控异常会向程序员发出信号,表明他实现了错误的东西。

正如人们已经说过的,Java字节码中不存在受控异常。它们只是一种编译器机制,与其他语法检查没有什么不同。我看到很多受控异常,就像我看到编译器抱怨一个冗余的条件:if(true) {a;b;}。这很有帮助,但我可能是故意这么做的,所以我忽略你的警告。

事实是,如果你强制执行受控异常,你将无法强迫每个程序员“做正确的事情”,而其他人现在都是附带损害,他们只是因为你制定的规则而讨厌你。

修复坏程序!不要试图修改语言来阻止它们!对于大多数人来说,“对异常做一些事情”实际上只是告诉用户它。我也可以告诉用户一个未检查的异常,所以不要让您的已检查异常类出现在我的API中。

SNR

首先,检查异常降低了代码的“信噪比”。Anders Hejlsberg也谈到了命令式编程和声明式编程,这是一个类似的概念。不管怎样,考虑下面的代码片段:

在Java中从非UI线程更新UI:

try {  
    // Run the update code on the Swing thread  
    SwingUtilities.invokeAndWait(() -> {  
        try {
            // Update UI value from the file system data  
            FileUtility f = new FileUtility();  
            uiComponent.setValue(f.readSomething());
        } catch (IOException e) {  
            throw new UncheckedIOException(e);
        }
    });
} catch (InterruptedException ex) {  
    throw new IllegalStateException("Interrupted updating UI", ex);  
} catch (InvocationTargetException ex) {
    throw new IllegalStateException("Invocation target exception updating UI", ex);
}

在c#中从非UI线程更新UI:

private void UpdateValue()  
{  
   // Ensure the update happens on the UI thread  
   if (InvokeRequired)  
   {  
       Invoke(new MethodInvoker(UpdateValue));  
   }  
   else  
   {  
       // Update UI value from the file system data  
       FileUtility f = new FileUtility();  
       uiComponent.Value = f.ReadSomething();  
   }  
}  

这对我来说清楚多了。当您开始在Swing中做越来越多的UI工作时,检查异常开始变得非常烦人和无用。

监狱打破

即使要实现最基本的实现,比如Java的List接口,作为契约式设计工具的受控异常也行不通。考虑一个由数据库、文件系统或任何其他抛出受控异常的实现支持的列表。唯一可能的实现是捕获已检查异常,并将其作为未检查异常重新抛出:

@Override
public void clear()  
{  
   try  
   {  
       backingImplementation.clear();  
   }  
   catch (CheckedBackingImplException ex)  
   {  
       throw new IllegalStateException("Error clearing underlying list.", ex);  
   }  
}  

现在你要问所有这些代码的意义是什么?被检查的异常只是增加了噪音,异常被捕获但没有处理,契约式设计(就被检查的异常而言)已经失效。

结论

捕获异常与处理异常是不同的。 受控异常会给代码添加噪音。 没有它们,异常处理在c#中也能很好地工作。

我之前在博客上写过。

程序员需要知道一个方法可能抛出的所有异常,以便正确地使用它。因此,仅仅用一些异常来打击他并不一定能帮助一个粗心的程序员避免错误。

微小的好处被繁重的成本所抵消(特别是在较大、不太灵活的代码库中,不断修改接口签名是不切实际的)。

Static analysis can be nice, but truly reliable static analysis often inflexibly demands strict work from the programmer. There is a cost-benefit calculation, and the bar needs to be set high for a check that leads to a compile time error. It would be more helpful if the IDE took on the role of communicating which exceptions a method may throw (including which are unavoidable). Although perhaps it would not be as reliable without forced exception declarations, most exceptions would still be declared in documentation, and the reliability of an IDE warning is not so crucial.

关于受控异常的问题是,按照通常的概念理解,它们并不是真正的异常。相反,它们是API可选返回值。

The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for. eg.[pseudocode]:

public [int or IOException] writeToStream(OutputStream stream) {
    [void or IOException] a= stream.write(mybytes);
    if (a instanceof IOException)
        return a;
    return mybytes.length;
}

由于Java不能选择返回值,或简单的内联元组作为返回值,受控异常是一个合理的响应。

问题是,很多代码,包括大量标准库,在真正的异常情况下滥用了检查过的异常,您可能非常想要在几个级别上捕获这些异常。为什么IOException不是RuntimeException?在其他任何语言中,我都可以让IO异常发生,如果我不做任何处理,我的应用程序将停止,我将获得一个方便的堆栈跟踪来查看。这是最好的结果了。

也许在这个例子中有两个方法,你想从整个写入到流的过程中捕获所有的ioexception,中止该过程并跳转到错误报告代码;在Java中,如果不在每个调用级别添加' throws IOException ',甚至是本身不执行IO的级别,就无法做到这一点。这样的方法不需要知道异常处理;必须为他们的签名添加异常:

不必要地增加耦合; 使接口签名非常容易更改; 使代码可读性降低; 是如此令人讨厌,以至于常见的程序员反应是通过做一些可怕的事情来击败系统,比如' throws Exception ', ' catch (Exception e){} ',或者将所有内容包装在RuntimeException中(这使得调试更加困难)。

然后还有很多可笑的库异常,比如:

try {
    httpconn.setRequestMethod("POST");
} catch (ProtocolException e) {
    throw new CanNeverHappenException("oh dear!");
}

当你不得不用这样可笑的东西把你的代码弄得乱七八糟的时候,也难怪受控异常会受到很多人的讨厌,尽管实际上这只是简单的糟糕的API设计。

另一个特别坏的影响是在控制反转,在组件提供一个回调通用组件B组件希望能够让一个异常抛出的回调回到的地方它称为组件B,但不能因为这样会改变固定的回调接口通过B只能做包装RuntimeException真正的例外,这是更多的异常处理样板来写。

在Java及其标准库中实现的受控异常意味着样板文件、样板文件、样板文件。在一个已经啰嗦的语言中,这不是一个胜利。