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


当前回答

我想我读过和你一样的布鲁斯·埃克尔的采访,它总是困扰着我。事实上,这个观点是由。net和c#背后的微软天才Anders Hejlsberg(如果这确实是你在谈论的帖子的话)提出的。

http://www.artima.com/intv/handcuffs.html

虽然我是海尔斯伯格和他的作品的粉丝,但我一直认为这种观点是虚假的。基本上可以归结为:

“受控异常很糟糕,因为程序员只是滥用它们,总是捕获它们并忽略它们,这导致问题被隐藏和忽略,否则将呈现给用户”。

通过“以其他方式呈现给用户”,我的意思是如果你使用了一个运行时异常,懒惰的程序员会忽略它(而不是用空的catch块捕获它),用户会看到它。

这个论点的总结是“程序员不会正确地使用它们,而不正确地使用它们比没有它们更糟糕”。

这种说法有一定道理,事实上,我怀疑Goslings不把运算符重写放在Java中的动机也是出于类似的理由——它们让程序员感到困惑,因为它们经常被滥用。

但最终,我发现这是海尔斯伯格的一个虚假论点,可能是一个事后的论点,用来解释这种缺乏,而不是一个经过深思熟虑的决定。

我想说的是,过度使用受控异常是一件坏事,容易导致用户处理草率,但是正确使用受控异常可以让API程序员给API客户端程序员带来巨大的好处。

现在API程序员必须小心不要到处抛出检查过的异常,否则它们只会惹恼客户端程序员。非常懒惰的客户端程序员将求助于catch (Exception){},正如Hejlsberg警告的那样,所有的好处都将失去,地狱将随之而来。 但在某些情况下,没有什么可以替代良好的受控异常。

For me, the classic example is the file-open API. Every programming language in the history of languages (on file systems at least) has an API somewhere that lets you open a file. And every client programmer using this API knows that they have to deal with the case that the file they are trying to open doesn't exist. Let me rephrase that: Every client programmer using this API should know that they have to deal with this case. And there's the rub: can the API programmer help them know they should deal with it through commenting alone or can they indeed insist the client deal with it.

在C语言中,这个习语是这样的

  if (f = fopen("goodluckfindingthisfile")) { ... } 
  else { // file not found ...

其中fopen通过返回0指示失败,而C(愚蠢地)让您将0视为布尔值和…基本上,你学会了这个习语就没事了。但如果你是个新手,没有学过这个习语呢?然后,当然,你开始用

   f = fopen("goodluckfindingthisfile");
   f.read(); // BANG! 

从艰苦中学习。

请注意,这里我们只讨论强类型语言:对于强类型语言中的API是什么有一个清晰的概念:它是供您使用的功能(方法)的大杂烩,并为每个功能(方法)使用明确定义的协议。

明确定义的协议通常由方法签名定义。 这里fopen要求你给它传递一个字符串(在C语言中是char*)。如果你给它其他东西,你会得到一个编译时错误。您没有遵循协议—您没有正确地使用API。

在一些(晦涩的)语言中,返回类型也是协议的一部分。如果你试图在某些语言中调用等同于fopen()的函数而不将其赋值给变量,你也会得到一个编译时错误(你只能用void函数来做)。

我想说的是:在静态类型语言中,API程序员鼓励客户端正确使用API,如果客户端代码出现明显错误,就会阻止它被编译。

(在像Ruby这样的动态类型语言中,你可以传递任何东西,比如float,作为文件名——它会被编译。如果你甚至不打算控制方法参数,为什么要用受控异常来麻烦用户呢?这里的参数只适用于静态类型的语言。)

那么,受控异常呢?

这里有一个可以用来打开文件的Java api。

try {
  f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
  // deal with it. No really, deal with it!
  ... // this is me dealing with it
}

看到了吗?下面是API方法的签名:

public FileInputStream(String name)
                throws FileNotFoundException

注意,FileNotFoundException是一个受控异常。

API程序员对你说: 你可以使用这个构造函数来创建一个新的FileInputStream

A)必须将文件名作为A 字符串 B)必须接受 文件可能不存在的可能性 在运行时被发现”

这就是我所关心的全部。

问题的关键在于问题所表述的“程序员无法控制的事情”。我的第一个想法是他/她指的是API程序员无法控制的东西。但事实上,受控异常在正确使用的情况下,确实应该用于客户端程序员和API程序员都无法控制的事情。我认为这是不滥用受控异常的关键。

我认为文件打开很好地说明了这一点。API程序员知道,您可能会给他们一个在调用API时不存在的文件名,并且他们将无法返回您想要的结果,而必须抛出异常。他们也知道这种情况会经常发生,客户端程序员在编写调用时可能希望文件名是正确的,但在运行时由于他们无法控制的原因也可能是错误的。

因此,API明确了这一点:在某些情况下,当你打电话给我时,这个文件不存在,你最好处理它。

有了反案例,这一点就更清楚了。假设我在写一个表API。我有一个包含这个方法的API的表模型:

public RowData getRowData(int row) 

现在,作为一个API程序员,我知道会有这样的情况:一些客户端会为行传递一个负值,或者在表外传递一个行值。所以我可能会抛出一个受控异常,并迫使客户端处理它:

public RowData getRowData(int row) throws CheckedInvalidRowNumberException

(当然,我不会称之为“检查过”。)

这是对受控异常的错误使用。客户端代码将充满获取行数据的调用,每个调用都必须使用try/catch,用于什么?它们是否会向用户报告查找了错误的行?可能不会,因为无论我的表格视图周围的UI是什么,它都不应该让用户进入一个非法行被请求的状态。所以这是客户端程序员的错误。

API程序员仍然可以预测客户端将编写这样的错误,并且应该用IllegalArgumentException这样的运行时异常来处理它。

对于getRowData中的受控异常,这显然会导致Hejlsberg的懒惰程序员只是添加空捕获。当这种情况发生时,即使对测试人员或客户端开发人员调试,非法行值也不会很明显,相反,它们会导致难以查明来源的直接错误。阿里安火箭发射后会爆炸。

好了,问题来了:我说的是检查异常FileNotFoundException不仅是一个好东西,而且是API程序员工具箱中的一个基本工具,它可以以对客户端程序员最有用的方式定义API。但是CheckedInvalidRowNumberException非常不方便,会导致糟糕的编程,应该避免使用。但是如何区分呢?

我想这并不是一门精确的科学我想这在一定程度上支持了海尔斯伯格的观点。但我不喜欢把孩子和洗澡水一起倒掉,所以请允许我在这里提取一些规则来区分好的受控异常和坏的异常:

Out of client's control or Closed vs Open: Checked exceptions should only be used where the error case is out of control of both the API and the client programmer. This has to do with how open or closed the system is. In a constrained UI where the client programmer has control, say, over all of the buttons, keyboard commands etc that add and delete rows from the table view (a closed system), it is a client programming bug if it attempts to fetch data from a nonexistent row. In a file-based operating system where any number of users/applications can add and delete files (an open system), it is conceivable that the file the client is requesting has been deleted without their knowledge so they should be expected to deal with it. Ubiquity: Checked exceptions should not be used on an API call that is made frequently by the client. By frequently I mean from a lot of places in the client code - not frequently in time. So a client code doesn't tend to try to open the same file a lot, but my table view gets RowData all over the place from different methods. In particular, I'm going to be writing a lot of code like if (model.getRowData().getCell(0).isEmpty())

而且,每次都必须在尝试/捕获中结束,这将是痛苦的。

Informing the User: Checked exceptions should be used in cases where you can imagine a useful error message being presented to the end user. This is the "and what will you do when it happens?" question I raised above. It also relates to item 1. Since you can predict that something outside of your client-API system might cause the file to not be there, you can reasonably tell the user about it: "Error: could not find the file 'goodluckfindingthisfile'" Since your illegal row number was caused by an internal bug and through no fault of the user, there's really no useful information you can give them. If your app doesn't let runtime exceptions fall through to the console it will probably end up giving them some ugly message like: "Internal error occured: IllegalArgumentException in ...." In short, if you don't think your client programmer can explain your exception in a way that helps the user, then you should probably not be using a checked exception.

这就是我的规则。有些刻意,毫无疑问会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在像FileNotFoundException这样的情况下,checked异常是API契约中与参数类型一样重要和有用的一部分。因此,我们不应该仅仅因为它被滥用就放弃它。

抱歉,我不是故意说这么长时间的。最后我想提两点建议:

答:API程序员:谨慎使用受控异常以保持它们的有用性。如果有疑问,请使用未检查的异常。

B:客户端程序员:养成在开发早期创建封装异常(谷歌)的习惯。JDK 1.4及后续版本在RuntimeException中为此提供了一个构造函数,但您也可以轻松地创建自己的构造函数。下面是构造函数:

public RuntimeException(Throwable cause)

然后养成这样的习惯:当您不得不处理受控异常时,您感到懒惰(或者您认为API程序员在第一时间使用受控异常时过于热心),不要只是吞下异常,包装它并重新抛出它。

try {
  overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
  throw new RuntimeException(exception);  
}

把它放在IDE的一个小代码模板中,当你觉得懒的时候就可以使用它。这样,如果您真的需要处理检查异常,那么在运行时看到问题后,您将被迫返回并处理它。因为,相信我(和安德斯·海尔斯伯格),你永远不会回到你的TODO

catch (Exception e) { /* TODO deal with this at some point (yeah right) */}

其他回答

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

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

在代码中失败通常是可能的,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/

Ok... Checked exceptions are not ideal and have some caveat but they do serve a purpose. When creating an API there are specific cases of failures that are contractual of this API. When in the context of a strongly statically typed language such as Java if one does not use checked exceptions then one must rely on ad-hoc documentation and convention to convey the possibility of error. Doing so removes all benefit that the compiler can bring in handling error and you are left completely to the good will of programmers.

因此,一个人删除了Checked异常,比如在c#中所做的,那么如何以编程和结构的方式传达错误的可能性呢?如何通知客户端代码,这样那样的错误可能发生,必须处理?

在处理受控异常时,我听到了各种可怕的事情,它们被滥用了,这是肯定的,但未受控异常也是如此。我说,等几年,当api被堆叠在很多层的时候,你会乞求某种结构化的方法来传达失败。

以异常在API层底部某处抛出的情况为例,因为没有人知道这个错误甚至可能发生,即使它是一种非常合理的错误类型,当调用代码抛出它时(例如FileNotFoundException而不是VogonsTrashingEarthExcept…)在这种情况下,我们是否处理它并不重要,因为没有任何东西可以处理它)。

Many have argued that not being able to load the file was almost always the end of the world for the process and it must die a horrible and painful death. So yeah.. sure ... ok.. you build an API for something and it loads file at some point... I as the user of said API can only respond... "Who the hell are you to decide when my program should crash !" Sure Given the choice where exceptions are gobbled up and leave no trace or the EletroFlabbingChunkFluxManifoldChuggingException with a stack trace deeper than the Marianna trench I would take the latter without a cinch of hesitation, but does this mean that it is the desirable way to deal with exception ? Can we not be somewhere in the middle, where the exception would be recast and wrapped each time it traversed into a new level of abstraction so that it actually means something ?

最后,我看到的大多数争论都是“我不想处理异常,许多人不想处理异常。受控异常迫使我去处理它们,因此我讨厌受控异常。”完全消除这种机制并将其降级到地狱的深渊是愚蠢的,缺乏判断力和远见。

如果我们消除了受控异常,我们也可以消除函数的返回类型,并且总是返回一个“anytype”变量……这样生活就简单多了,不是吗?

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

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

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

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

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#中也能很好地工作。

我之前在博客上写过。

这并不是反对受控异常的纯概念,但是Java用于受控异常的类层次结构是一个畸形秀。我们总是简单地称这些东西为“异常”——这是正确的,因为语言规范也这样称呼它们——但是异常在类型系统中是如何命名和表示的呢?

By the class Exception one imagines? Well no, because Exceptions are exceptions, and likewise exceptions are Exceptions, except for those exceptions that are not Exceptions, because other exceptions are actually Errors, which are the other kind of exception, a kind of extra-exceptional exception that should never happen except when it does, and which you should never catch except sometimes you have to. Except that's not all because you can also define other exceptions that are neither Exceptions nor Errors but merely Throwable exceptions.

哪些是“已检查”异常?可抛出异常是受控异常,除非它们也是错误,是未检查的异常,然后是异常,也是可抛出异常,是受控异常的主要类型,除了有一个例外,那就是if它们也是runtimeexception,因为那是另一种未检查的异常。

What are RuntimeExceptions for? Well just like the name implies, they're exceptions, like all Exceptions, and they happen at run-time, like all exceptions actually, except that RuntimeExceptions are exceptional compared to other run-time Exceptions because they aren't supposed to happen except when you make some silly error, although RuntimeExceptions are never Errors, so they're for things that are exceptionally erroneous but which aren't actually Errors. Except for RuntimeErrorException, which really is a RuntimeException for Errors. But aren't all exceptions supposed to represent erroneous circumstances anyway? Yes, all of them. Except for ThreadDeath, an exceptionally unexceptional exception, as the documentation explains that it's a "normal occurrence" and that that's why they made it a type of Error.

Anyway, since we're dividing all exceptions down the middle into Errors (which are for exceptional execution exceptions, so unchecked) and Exceptions (which are for less exceptional execution errors, so checked except when they're not), we now need two different kinds of each of several exceptions. So we need IllegalAccessError and IllegalAccessException, and InstantiationError and InstantiationException, and NoSuchFieldError and NoSuchFieldException, and NoSuchMethodError and NoSuchMethodException, and ZipError and ZipException.

只不过,即使检查了异常,也总有(相当简单的)方法可以欺骗编译器,在不检查的情况下抛出异常。如果你这样做,你可能会得到一个不确定的throwableexception,除非在其他情况下,它可能抛出一个意外的dexception,或一个未知的异常(与未知的错误无关,只针对“严重的异常”),或一个ExecutionException,或一个InvocationTargetException,或一个ExceptionInInitializerError。

哦,我们一定不能忘记Java 8时髦的新UncheckedIOException,这是一个RuntimeException异常,设计用来通过包装由I/O错误(不会引起IOError异常,尽管也存在)引起的已检查的IOException异常来抛出异常检查的概念,这些异常异常难以处理,因此您需要它们不被检查。

由于Java !