反对受控异常的情况

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

我目前正在编写一些新代码,并非常注意如何处理异常。我正试图从“我们不喜欢有限制的例外”的观点出发。人群拥挤,我仍然看不到它。

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

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

  • Error是不应该被抓到的东西(VM有花生过敏,有人掉了一罐花生在上面)
  • RuntimeException是程序员做错的事情(程序员离开数组的末尾)
  • Exception(除了RuntimeException)是用于程序员无法控制的事情(磁盘在写入文件系统时被填满,进程的文件句柄限制已经达到,你不能再打开任何文件)
  • Throwable只是所有异常类型的父类。

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

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

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

因为“它使重构变得困难”;这说明没有选择正确的抽象层次。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。

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

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

如果你抛出RuntimeException 而不是Exception 子类,你怎么知道 你应该去抓?< / p >

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

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

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

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

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

我不是在寻找关于何时使用这两种模型的建议,我在寻找的是为什么人们从RuntimeException扩展,因为他们不喜欢从Exception扩展和/或为什么他们捕捉到异常,然后重新抛出RuntimeException,而不是向他们的方法添加抛出。我想了解不喜欢受控异常的动机。

78436 次浏览

我最初同意你的观点,因为我一直支持受控异常,并开始思考为什么我不喜欢在. net中没有受控异常。但后来我意识到我并不喜欢受控异常。

回答您的问题,是的,我喜欢我的程序显示堆栈跟踪,最好是非常难看的跟踪。我希望应用程序爆发成一堆您希望看到的最糟糕的错误消息。

原因是,如果出现这种情况,我必须修复它,而且必须马上修复。我想马上知道有什么问题。

您实际处理了多少次异常?我说的不是捕获异常——我说的是处理异常?这样写太简单了:

try {
thirdPartyMethod();
} catch(TPException e) {
// this should never happen
}

我知道你可能会说这是一种糟糕的实践,“答案”是做一些异常(让我猜猜,记录它?),但在现实世界(tm)中,大多数程序员就是不这样做。

所以,是的,我不想捕捉异常,如果我没有必要这样做,我希望我的程序在我搞砸的时候爆炸。默默的失败是最糟糕的结果。

在这里反对受控异常的一个参数(from joelonsoftware.com):

理由是我认为异常并不比 "goto's"自20世纪60年代以来就被认为是有害的,因为它们会产生一种 从一个代码点突然跳转到另一个代码点。事实上他们是 明显比goto差:

    它们在源代码中是不可见的。看着一段代码, 包括可能抛出或不抛出异常的函数,没有 查看哪些异常可能被抛出以及从哪里抛出的方法。这意味着 即使仔细的代码检查也不会发现潜在的错误 它们为一个函数创建了太多可能的出口点。正确书写 代码,你必须考虑每一个可能的代码路径 你的功能。每次你调用一个可以引发 异常而不要当场抓住它,你为之创造机会 由于函数突然终止而导致的意外错误 数据处于不一致的状态,或其他代码路径不一致 李思考。< / >

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

到目前为止,我发现使用受控异常更容易处理代码库。当我使用别人的API时,当我调用代码并通过记录、显示或忽略(是的,有忽略异常的有效情况,例如ClassLoader实现)来正确处理它们时,我可以准确地看到我可以预期的错误条件类型,这很好。这给了我正在编写的代码一个恢复的机会。我向上传播所有运行时异常,直到它们被缓存并使用一些通用的错误处理代码进行处理。当我发现一个我不想在特定级别上处理的受控异常,或者我认为是一个编程逻辑错误时,我将它包装成RuntimeException并让它冒出来。永远不要在没有充分理由的情况下接受异常(这样做的充分理由相当少)

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

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

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

好吧,这不是关于显示堆栈跟踪或无声崩溃。它是关于在层与层之间沟通错误的能力。

受控异常的问题在于,它们鼓励人们忽略重要的细节(即异常类)。如果你选择不接受这些细节,那么你必须在整个应用程序中不断添加throws声明。这意味着1)一个新的异常类型将影响许多函数签名,2)你可能会错过你实际上想要捕获的异常的特定实例(比如你打开了一个将数据写入文件的函数的次要文件)。次要文件是可选的,所以你可以忽略它的错误,但由于签名throws IOException,很容易忽略这一点)。

实际上,我现在在一个应用程序中处理这种情况。我们将异常重新打包为AppSpecificException。这使得签名非常干净,我们不必担心签名中的throws爆炸。

当然,现在我们需要在更高的级别上专门化错误处理,实现重试逻辑等等。所有的东西都是AppSpecificException,所以我们不能说“如果一个IOException被抛出,重试”或“如果ClassNotFound被抛出,完全中止”。我们没有获得真正的异常的可靠方法,因为当它们在我们的代码和第三方代码之间传递时,东西会一次又一次地重新打包。

这就是为什么我非常喜欢python中的异常处理。你只能捕获你想要和/或能处理的东西。其他所有东西都冒出来了,就好像你自己重新扔了它一样(不管怎样你已经这么做了)。

我一次又一次地发现,在我提到的整个项目中,异常处理分为3类:

  1. 捕获并处理具体的异常。例如,这是为了实现重试逻辑。
  2. 捕获并重新抛出其他异常。这里所发生的一切通常是日志记录,它通常是一个老套的消息,如“无法打开$filename”。这些都是你无能为力的错误;只有更高层次的人才知道如何处理。
  3. 捕获所有内容并显示错误消息。这通常位于调度程序的最根,它所做的一切都是确保它可以通过非异常机制(弹出对话框、封送RPC error对象等)将错误传递给调用者。

我认为这是一个很好的问题,没有任何争论。我认为第三方库(通常)应该抛出无节制的异常。这意味着你可以隔离你对库的依赖(即你不必重新抛出它们的异常或抛出Exception——通常是坏习惯)。Spring的DAO层就是一个很好的例子。

另一方面,通常应该检查来自核心Java API的异常是否可以处理。比如FileNotFoundException或者(我最喜欢的)InterruptedException。这些情况应该特别处理几乎总是(即你对InterruptedException的反应与你对IllegalArgumentException的反应不同)。异常被检查的事实迫使开发人员考虑条件是否可处理。(也就是说,我很少看到InterruptedException被正确处理!)

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

Anders在软件工程电台的集97中谈到了受控异常的陷阱,以及他为什么把它们排除在c#之外。

Artima 发表采访和。net的架构师之一Anders Hejlsberg一起编写的,它敏锐地涵盖了反对受控异常的参数。品性差的人:

throws子句(至少从它在Java中的实现方式来看)并不一定会强制您处理异常,但如果不处理它们,则会强制您准确地确认可能通过哪些异常。它要求你要么捕获声明的异常,要么把它们放在你自己的throws子句中。为了满足这个要求,人们会做一些可笑的事情。例如,它们用“throws Exception”修饰每个方法。这完全破坏了这个功能,你只会让程序员写更多晦涩难懂的东西。这对任何人都没有帮助。

简而言之:

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

检查异常的参数:

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

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

因此,参数检查异常是编译器强制程序员声明抛出了哪些异常,然后程序员还将记录特定的异常类和导致它们的错误。

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

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

所以为什么要麻烦呢?

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

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

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

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

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

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

异常的整个思想是,在调用链的某个位置抛出的错误可以向上冒泡,并由更高位置的代码处理,而不需要中间的代码担心它。另一方面,受控异常要求抛出器和捕获器之间的每一级代码声明它们知道可以通过它们的所有形式的异常。在实践中,这与检查的异常仅仅是调用者必须检查的特殊返回值没有什么不同。例如。(伪代码):

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的级别,就无法做到这一点。这样的方法不需要知道异常处理;必须为他们的签名添加异常:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

该参数的摘要是“程序员不会正确地使用它们,而不正确地使用它们比没有它们更糟糕”;. c。

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

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

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

现在API程序员必须小心不要到处抛出检查过的异常,否则它们只会惹恼客户端程序员。非常懒惰的客户端程序员会像Hejlsberg警告的那样去捕捉(Exception) {},所有的好处都将失去,地狱将随之而来。 但在某些情况下,没有什么可以替代良好的受控异常 对我来说,经典的例子是文件打开API。语言历史上的每一种编程语言(至少在文件系统上)都有一个允许您打开文件的API。每个使用这个API的客户端程序员都知道,他们必须处理他们试图打开的文件不存在的情况。 让我重新表述一下:每个使用这个API 应该知道的客户端程序员都必须处理这种情况。 这里有一个问题:API程序员是否可以帮助他们知道他们应该单独通过注释来处理它,或者他们确实可以坚持客户端处理它

在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非常不方便,会导致糟糕的编程,应该避免使用。但是如何区分呢?

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

  1. 超出客户端控制或关闭vs打开:

    受控异常只能在错误情况超出API 而且和客户端程序员控制的情况下使用。 这与系统的开放关闭有关。在限制 UI中,客户端程序员可以控制所有的按钮,键盘命令等,从表视图(一个封闭的系统)中添加和删除行,如果它试图从不存在的行中获取数据,这是一个客户端编程错误。在基于文件的操作系统中,任意数量的用户/应用程序都可以添加和删除文件(一个开放系统),可以想象的是,客户端请求的文件已经在他们不知情的情况下被删除了,因此他们应该被期望处理它

  2. < p >无处不在:

    受控异常不应该用于客户端频繁进行的API调用。 我所说的频繁是指从客户端代码中的很多地方——不是频繁地在时间上。所以客户端代码不会经常打开同一个文件,但是我的表格视图会从不同的方法得到RowData。特别地,我会写很多

    这样的代码
    if (model.getRowData().getCell(0).isEmpty())
    

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

  1. 通知用户:

    受控异常应该在你可以想象一个有用的错误消息被呈现给最终用户的情况下使用。 这是我上面提出的“当它发生时,你会做什么?”问题。它也与项目1有关。因为您可以预测客户端api系统之外的某些东西可能导致文件不存在,所以您可以合理地告诉用户:

    "Error: could not find the file 'goodluckfindingthisfile'"
    

    由于非法的行号是由内部错误引起的,而不是用户的过错,因此实际上没有任何有用的信息可以提供给他们。如果你的应用不让运行时异常出现在控制台,那么最终可能会给它们一些丑陋的消息,比如:

    "Internal error occured: IllegalArgumentException in ...."
    

    简而言之,如果您认为您的客户程序员不能以一种帮助用户的方式解释您的异常,那么您可能不应该使用受控异常。

这就是我的规则。有些刻意,毫无疑问会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在像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) */}

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

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

OOPSLA 2005年的文件(或相关技术报告.)中描述了一个采取中间路线的好方法。

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

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

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

< >强应变: 一种期望的条件,要求方法提供可根据该方法的预期目的来表达的替代响应。方法的调用者期望这些类型的条件,并有一个策略来应对它们

< >强错: 一种计划外的情况,阻止方法实现其预期目的,如果不参考方法的内部实现就无法描述

(SO不允许表,所以你可能想从原始页面…读取以下内容)

应急

  • 被认为是:设计的一部分
  • 预期会发生:经常发生但很少发生
  • 谁关心它:调用方法的上游代码
  • 示例:可选返回模式
  • 最佳映射:检查异常

的错

  • 被认为是:一个令人讨厌的惊喜
  • 可能发生:永远不会
  • 谁关心它:需要解决问题的人吗
  • 示例:编程错误,硬件故障,配置错误, 文件丢失,服务器不可用
  • 最佳映射:未检查的异常

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

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

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

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

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

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

信噪比

首先,检查异常降低了代码的“信噪比”。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字节码中不存在受控异常。它们只是一种编译器机制,与其他语法检查没有什么不同。我看到很多受控异常,就像我看到编译器抱怨一个多余的条件:if(true) { a; } b;。这很有帮助,但我可能是故意这么做的,所以我忽略你的警告。

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

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

我读了很多关于异常处理的书,即使(大多数时候)我不能真的说我对受控异常的存在感到高兴或悲伤,这是我的看法:在低级代码(IO,网络,OS等)中受控异常,在高级api /应用程序级别中未受控异常。

即使在它们之间没有那么容易划清界限,我发现在同一屋檐下集成几个api /库而不始终包装大量的检查异常是非常烦人/困难的,但另一方面,有时强制捕获一些异常并提供一个在当前上下文中更有意义的不同异常是有用/更好的。

我正在做的项目需要很多库,并将它们集成到同一个API下,API完全基于未检查的异常。这个框架提供了一个高级API,在一开始充满了受控异常,只有几个未受控异常(初始化异常,配置异常等),我必须说不是很友好的。大多数时候,您必须捕获或重新抛出您不知道如何处理的异常,或者您甚至不关心(不要混淆,您应该忽略异常),特别是在客户端,一次单击可能抛出10个可能的(已选中的)异常。

当前版本(第三个版本)只使用未检查的异常,并且它有一个全局异常处理程序,负责处理任何未捕获的异常。API提供了一种注册异常处理程序的方法,它将决定一个异常是否被认为是错误(大多数情况下是这样),这意味着日志&通知某人,或者它可以意味着其他的东西-像这个异常,AbortException,这意味着中断当前的执行线程,不要记录任何错误,因为它不希望这样做。当然,为了解决所有自定义线程必须处理run()方法与一个try{…}(所有)。

公共无效运行(){

try {
... do something ...
} catch (Throwable throwable) {
ApplicationContext.getExceptionService().handleException("Handle this exception", throwable);
}

如果您使用WorkerService来安排作业(Runnable, Callable, Worker),这是不必要的,它会为您处理一切。

当然,这只是我的个人观点,它可能不是正确的,但对我来说这是一个很好的方法。我将在发布项目后看看我认为对我有好处的东西,对其他人也有好处……:)

好吧……受控异常并不理想,有一些警告,但它们确实起到了一定的作用。在创建API时,有一些特定的失败案例是这个API的契约性失败。在强静态类型语言(如Java)的上下文中,如果不使用受控异常,则必须依赖特定的文档和约定来传达错误的可能性。这样做消除了编译器在处理错误方面可以带来的所有好处,你完全被程序员的善意所抛弃。

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

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

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

许多人认为,无法加载文件几乎总是这个过程的世界末日,它必须死于可怕和痛苦的死亡。是的. .确定……好吧. .你为某个东西构建了一个API,它在某个时候加载文件……我作为该API的用户只能回应…“你到底是谁,决定我的程序什么时候崩溃!”当然,如果有两个选择,一个是异常被吞噬,没有留下任何痕迹,另一个是EletroFlabbingChunkFluxManifoldChuggingException,堆栈跟踪比Marianna壕更深,我会毫不犹豫地选择后者,但这是否意味着它是处理异常的理想方式?难道我们不能处于中间的某个位置吗?在那里,每当异常遍历到一个新的抽象级别时,它都会被重铸和包装,这样它实际上就有意义了。

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

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

我在c2.com上写的东西与最初的形式CheckedExceptionsAreIncompatibleWithVisitorPattern基本没有变化

总而言之:

访问者模式(Visitor Pattern)及其亲属是一类接口,其中间接调用者和接口实现都知道异常,但接口和直接调用者组成了一个不知道异常的库。

CheckedExceptions的基本假设是,所有声明的异常都可以从调用带有该声明的方法的任何点抛出。VisitorPattern揭示了这个假设是错误的。

在这种情况下,受控异常的最终结果是大量无用的代码,本质上是在运行时删除编译器的受控异常约束。

至于根本问题:

我的总体想法是,顶级处理程序需要解释异常并显示适当的错误消息。我几乎总是看到IO异常、通信异常(由于某些原因api可以区分)或任务致命错误(程序错误或备份服务器上的严重问题),所以如果我们允许对严重的服务器问题进行堆栈跟踪,这应该不会太难。

尽管阅读了整页,我仍然找不到一个反对受控异常的合理论点。相反,大多数人都在谈论糟糕的API设计,无论是在一些Java类中还是在他们自己的类中。

这个功能唯一令人讨厌的地方就是原型设计。这可以通过向语言中添加一些机制来解决(例如,一些@supresscheckedexceptions注释)。但是对于常规编程,我认为受控异常是一件好事。

这个问题

我在异常处理机制中看到的最糟糕的问题是它引入了大规模的代码复制!让我们诚实地说:在大多数项目中,在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注释)。

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

最后一点

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

这篇文章是我读过的关于Java异常处理的最好的一篇文章。

它更倾向于未检查的异常,而不是已检查的异常,但这个选择的解释非常透彻,并基于强有力的论据。

我不想在这里引用太多的文章内容(最好是整体阅读),但它涵盖了这个线程中未检查异常倡导者的大部分论点。尤其是这个论点(似乎很受欢迎):

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

作者“回应”:

这是绝对不正确的假设所有运行时异常 不应该被抓住,让它传播到最“顶端”吗 应用程序。(…)每一种特殊情况都需要 由系统/业务需求明确地处理 程序员必须决定在哪里捕获它,以及一旦被捕获该做什么 条件被捕获。这必须严格按照 应用程序的实际需求,而不是基于编译器警报。所有 其他错误必须允许自由传播到最上面 处理程序,它们将被记录在哪里,以及一个优雅的(也许,

.

.

主要的思想或文章是:

当涉及到软件中的错误处理时,唯一安全且正确的假设是,存在的每个子程序或模块都可能发生故障!

因此,如果“甚至没有人知道会发生这种错误”表示该项目有问题。像作者建议的那样,这种异常至少应该由最通用的异常处理程序来处理(例如,处理所有没有更特定的处理程序处理的异常)。

很遗憾,似乎没有多少人发现这篇伟大的文章:-(。我衷心建议每一个犹豫哪种方法更好的人花点时间阅读它。

试图解决这个尚未解决的问题:

如果抛出RuntimeException子类而不是Exception子类,那么您如何知道应该捕获什么?

恕我直言,这个问题包含似是而非的推理。仅仅因为API告诉你它抛出了什么并不意味着你在所有情况下都以相同的方式处理它。 换句话说,您需要捕获的异常取决于您使用抛出异常的组件的上下文

例如:

如果我正在为数据库编写连接测试程序,或者编写检查用户输入XPath的有效性的程序,那么我可能希望捕获并报告操作抛出的所有已检查和未检查的异常。

然而,如果我正在编写一个处理引擎,我可能会以与NPE相同的方式处理XPathException (checked):我将让它运行到工作线程的顶部,跳过该批处理的其余部分,记录问题(或将其发送给支持部门进行诊断),并为用户联系支持人员留下反馈。

我们已经看到了c#首席架构师的一些参考。

下面是一个Java人关于何时使用受控异常的另一种观点。他承认了其他人提到的许多负面因素: 有效的异常 < / p >

良好的证明Checked Exception是不需要的:

  1. 很多框架为Java做了一些工作。就像Spring将JDBC异常包装为未检查的异常,将消息抛出到日志中
  2. 很多在java之后出现的语言,甚至在java平台之上——他们都不使用它们
  3. 受控异常,它是对客户端将如何使用抛出异常的代码的一种预测。但是编写这段代码的开发人员永远不会知道代码的客户端正在使用的系统和业务。作为示例,接口方法强制抛出检查异常。系统中有100个实现,50甚至90个实现不抛出这个异常,但是如果用户引用了该接口,客户端仍然必须捕获这个异常。这50或90个实现倾向于在自身内部处理这些异常,将异常记录到日志中(这对它们来说是一种良好的行为)。我们该怎么做呢?我最好有一些后台逻辑来完成所有的工作——向日志发送消息。如果我作为代码的客户端,觉得需要处理异常,我就会去做。我可能会忘记它,但如果我使用TDD,我的所有步骤都被覆盖了,我知道我想要什么。
  4. 另一个例子,当我在java中使用I/O时,它迫使我检查所有异常,如果文件不存在?我该怎么做呢?如果不存在,则不会进行下一步操作。这个方法的客户端,不会从那个文件中得到预期的内容——他可以处理运行时异常,否则我应该先检查检查异常,把消息放到日志中,然后从方法中抛出异常。不…不-我最好用runtimeeption自动做它,它/自动点亮。手动处理它没有任何意义-我会很高兴在日志中看到一个错误消息(AOP可以帮助解决这个问题..修复java的一些东西)。如果,最终,我装置,系统应该显示弹出消息给最终用户-我将显示它,不是问题。

我很高兴,如果java能提供给我一个选择使用什么,当与核心库,如I/O。Like提供了相同类的两个副本——一个用RuntimeEception包装。然后我们可以比较人们会使用什么。但是现在,很多人会选择java或其他语言之上的框架。比如Scala, JRuby等等。许多人相信SUN是对的。

异常类

当谈到异常时,我总是参考埃里克·利珀特令人烦恼的例外博客文章。他将例外情况分为以下几类:

  • 致命的 -这些异常是不是你的错:你不能阻止它们,你也不能理智地处理它们。例如,OutOfMemoryErrorThreadAbortException
  • 愚蠢 -这些异常是你的错吗:你应该阻止他们,他们代表你的代码中的错误。例如,ArrayIndexOutOfBoundsExceptionNullPointerException或任何IllegalArgumentException
  • 令人烦恼的 -这些异常是不例外,不是你的错,你不能阻止他们,但你必须处理他们。它们通常是一个不幸的设计决策的结果,例如从Integer.parseInt抛出NumberFormatException,而不是提供一个在解析失败时返回布尔false的Integer.tryParseInt方法。
  • 外生 -这些异常通常都是例外,不是你的错,你不能(合理地)阻止它们,而是你必须处理它们。例如,FileNotFoundException

API用户:

  • 不得处理致命的愚蠢异常。
  • 应该处理令人烦恼的异常,但它们不应该出现在理想的API中。
  • 必须处理外生异常。

已检查的异常

API用户必须处理特定异常的事实是调用者和被调用者之间的方法契约的一部分。契约指定了被调用方期望的参数的数量和类型,调用方期望的返回值类型,以及调用者需要处理的异常

由于API中不应该存在令人烦恼的异常,因此只有这些外生异常必须是已检查的异常,才能成为方法契约的一部分。相对较少的异常是外生,所以任何API都应该有相对较少的受控异常。

已检查异常是必须处理. exe中的异常。处理异常就像吞下它一样简单。在那里!异常被处理。时期。如果开发者想这样处理,没问题。但他不能忽视这个例外,已经得到了警告。

API的问题

但是任何检查过令人烦恼的致命的异常的API(例如JCL)都会给API用户带来不必要的压力。这样的异常将被处理,但要么异常是如此普遍,它本来就不应该是一个异常,要么在处理它时什么都不能做。而导致Java开发人员讨厌受控异常。

此外,许多api没有适当的异常类层次结构,导致各种非外生异常原因都由单个受控异常类表示(例如IOException)。这也导致Java开发人员讨厌受控异常。

结论

外生异常是那些不是你的错,无法预防,应该处理。这些构成了所有可能引发的异常的一个小子集。api应该只具有检查外生异常,并且所有其他异常都未选中。这将使API更好,给API用户带来更少的压力,从而减少捕获所有、吞咽或重新抛出未经检查的异常的需要。

所以不要讨厌Java和它的受控异常。相反,讨厌那些过度使用受控异常的api。

在我看来,受控异常是一个非常好的概念。不幸的是,我们一起工作过的大多数程序员都有另一种看法,因此项目有很多错误的异常处理。我看到大多数程序员创建一个(只有一个)异常类,即RuntimeException的一个子类。包含一条消息,有时是一个多语言键。我没有机会反驳这一点。当我向他们解释什么是反模式,什么是方法的契约时,我有一种对着墙说话的感觉……我有点失望。

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

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

我知道这是一个老问题,但我花了一段时间与受控异常搏斗,我有一些东西要补充。请原谅我的长度!

我对受控异常的主要不满是它们破坏了多态性。让它们很好地与多态接口一起工作是不可能的。

以优秀的Java List接口为例。我们有常见的内存实现,如ArrayListLinkedList。我们还有骨架类AbstractList,它可以很容易地设计新的列表类型。对于只读列表,我们只需要实现两个方法:size()get(int index)

这个例子WidgetList类从一个文件中读取一些固定大小的Widget类型的对象(未显示):

class WidgetList extends AbstractList<Widget> {
private static final int SIZE_OF_WIDGET = 100;
private final RandomAccessFile file;


public WidgetList(RandomAccessFile file) {
this.file = file;
}


@Override
public int size() {
return (int)(file.length() / SIZE_OF_WIDGET);
}


@Override
public Widget get(int index) {
file.seek((long)index * SIZE_OF_WIDGET);
byte[] data = new byte[SIZE_OF_WIDGET];
file.read(data);
return new Widget(data);
}
}

通过使用熟悉的List接口公开widget,你可以检索条目(list.get(123))或迭代列表(for (Widget w : list) ...),而不需要知道WidgetList本身。可以将此列表传递给任何使用泛型列表的标准方法,或将其包装在Collections.synchronizedList中。使用它的代码既不需要知道也不需要关心“widget”是当场生成的,还是来自数组,还是从文件、数据库、网络或未来的子空间中继中读取的。它仍然可以正确工作,因为List接口被正确实现。

但事实并非如此。上面的类不能编译,因为文件访问方法可能会抛出IOException,一个你必须“捕获或指定”的检查异常。你不能指定它被抛出——编译器不会让你这样做,因为这会违反List接口的约定。并且WidgetList本身没有任何有用的方法可以处理异常(稍后我会详细说明)。

显然,唯一要做的事情是捕获并重新抛出已检查异常作为一些未检查的异常:

@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw new WidgetListException(e);
}
}


public static class WidgetListException extends RuntimeException {
public WidgetListException(Throwable cause) {
super(cause);
}
}

((编辑:Java 8已经为这种情况添加了一个UncheckedIOException类:用于跨多态方法边界捕获和重新抛出__abc1。有点证明了我的观点!))

在这种情况下检查异常根本不工作。你不能扔。同样,由数据库支持的聪明的Map,或通过COM端口连接到量子熵源的java.util.Random实现。当您试图用多态接口的实现做任何新奇的事情时,受控异常的概念就失效了。但是受控异常非常危险,它们仍然不能让您安静下来,因为您仍然必须捕获并重新抛出来自低层方法的异常,这会使代码和堆栈跟踪变得混乱。

我发现无处不在的Runnable接口经常被退回到这个角落,如果它调用了抛出检查异常的东西。它不能像现在一样抛出异常,所以它所能做的就是通过捕获并作为RuntimeException重新抛出来混乱代码。

实际上,如果你求助于黑客,你可以会抛出未声明的受控异常。JVM在运行时并不关心受控异常规则,因此我们只需要欺骗编译器。最简单的方法就是滥用泛型。这是我的方法(类名显示,因为(在Java 8之前)它是通用方法调用语法中必需的):

class Util {
/**
* Throws any {@link Throwable} without needing to declare it in the
* method's {@code throws} clause.
*
* <p>When calling, it is suggested to prepend this method by the
* {@code throw} keyword. This tells the compiler about the control flow,
* about reachable and unreachable code. (For example, you don't need to
* specify a method return value when throwing an exception.) To support
* this, this method has a return type of {@link RuntimeException},
* although it never returns anything.
*
* @param t the {@code Throwable} to throw
* @return nothing; this method never returns normally
* @throws Throwable that was provided to the method
* @throws NullPointerException if {@code t} is {@code null}
*/
public static RuntimeException sneakyThrow(Throwable t) {
return Util.<RuntimeException>sneakyThrow1(t);
}


@SuppressWarnings("unchecked")
private static <T extends Throwable> RuntimeException sneakyThrow1(
Throwable t) throws T {
throw (T)t;
}
}

华友世纪!使用此方法,我们可以在堆栈的任何深度抛出检查异常,而无需声明它,无需将其包装在RuntimeException中,并且不会混乱堆栈跟踪!再次使用"WidgetList"的例子:

@Override
public int size() {
try {
return (int)(file.length() / SIZE_OF_WIDGET);
} catch (IOException e) {
throw sneakyThrow(e);
}
}

不幸的是,受控异常的最后一种侮辱是,编译器拒绝允许你一个受控异常,如果在它有缺陷的观点中,它不能被抛出。(未检查的异常没有此规则。)要捕获偷偷抛出的异常,我们必须这样做:

try {
...
} catch (Throwable t) { // catch everything
if (t instanceof IOException) {
// handle it
...
} else {
// didn't want to catch this one; let it go
throw t;
}
}

这有点尴尬,但从好的方面来看,它仍然比提取包装在RuntimeException中的受控异常的代码略简单。

幸运的是,throw t;语句在这里是合法的,尽管t的类型是检查的,这要归功于Java 7中添加的关于重新抛出捕获的异常的规则。


当检查异常遇到多态性时,相反的情况也是一个问题:当一个方法被指定为可能抛出检查异常,但重写的实现不会。例如,抽象类OutputStreamwrite方法都指定了throws IOExceptionByteArrayOutputStream是一个子类,它写入内存中的数组,而不是真正的I/O源。它覆盖的write方法不会引起__abc5,所以它们没有throws子句,并且你可以调用它们而不用担心catch-or-specify要求。

但并非总是如此。假设Widget有一个保存到流的方法:

public void writeTo(OutputStream out) throws IOException;

声明这个方法接受普通的OutputStream是正确的做法,因此它可以多态地用于各种输出:文件、数据库、网络等等。以及内存数组。然而,对于内存中的数组,有一个虚假的要求来处理一个实际上不会发生的异常:

ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
someWidget.writeTo(out);
} catch (IOException e) {
// can't happen (although we shouldn't ignore it if it does)
throw new RuntimeException(e);
}

像往常一样,受控异常会成为阻碍。如果你的变量被声明为具有更多开放式异常需求的基类型,你必须为这些异常添加处理程序,即使你知道它们不会出现在你的应用程序中。

但是等等,检查异常实际上是所以恼人的,即他们甚至不让你反过来做!想象一下,你当前捕获了write调用在OutputStream上抛出的任何IOException,但你想将变量的声明类型更改为ByteArrayOutputStream,编译器将斥责你试图捕获它说不能抛出的检查异常。

那项规定引起了一些荒谬的问题。例如,OutputStream的三个write方法之一被ByteArrayOutputStream覆盖。具体来说,write(byte[] data)是一个方便的方法,它通过调用带有0偏移量和数组长度的write(byte[] data, int offset, int length)来写入完整的数组。ByteArrayOutputStream重写了三个参数的方法,但原样继承了一个参数的方便方法。继承的方法做了正确的事情,但它包含了一个不需要的throws子句。这可能是ByteArrayOutputStream设计中的一个疏忽,但他们永远无法修复它,因为它会破坏与任何捕获异常的代码的源代码兼容性——这个异常从来没有,从来没有,也永远不会抛出!

这个规则在编辑和调试过程中也很烦人。例如,有时我会临时注释掉一个方法调用,如果它本可以抛出一个检查异常,编译器现在就会抱怨存在局部trycatch块。所以我也必须注释掉这些,现在在编辑代码时,IDE将缩进到错误的级别,因为{}被注释掉了。嗨!这是一个小小的抱怨,但似乎受控异常所做的唯一一件事就是引起麻烦。


我快做完了。我对受控异常的最后挫折是在大多数呼叫站点,你可以用它们做任何有用的事情。理想情况下,当出现问题时,我们应该有一个称职的特定于应用程序的处理程序,可以通知用户问题和/或适当地结束或重试操作。只有堆栈中较高的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。

相反,我们得到了下面的习语,这是一种关闭编译器的猖獗方式:

try {
...
} catch (SomeStupidExceptionOmgWhoCares e) {
e.printStackTrace();
}

在GUI或自动化程序中,打印的消息将不会被看到。更糟糕的是,它在异常之后继续执行其余代码。异常实际上不是一个错误吗?那就别印出来。否则,马上就会出现其他异常,此时原始异常对象将消失。这个习惯用法不比BASIC的On Error Resume Next或PHP的error_reporting(0);好。

调用某种类型的记录器类也好不到哪里去:

try {
...
} catch (SomethingWeird e) {
logger.log(e);
}

这就像e.printStackTrace();一样懒惰,仍然在不确定的状态下处理代码。另外,特定日志系统或其他处理程序的选择是特定于应用程序的,因此这会影响代码重用。

但是等等!有一种简单而通用的方法可以找到特定于应用程序的处理程序。它位于调用堆栈的更高位置(或者它被设置为线程的未捕获异常处理程序)。所以在大多数地方,你所需要做的就是将异常抛出到堆栈的更高位置。例如,throw e;。受控异常只会碍事。

我确信,在设计语言时,受控异常听起来是个好主意,但在实践中,我发现它们都很麻烦,而且没有任何好处。

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

最初的概念中从来没有包括的,是强制一系列系统的&要声明的不可恢复故障。这些失败不能被声明为受控异常。

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

一个关键点是运行时,受控异常在功能上是等价的。没有检查异常可以做的处理或恢复,而运行时异常不能。

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

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

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

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

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

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

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

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

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

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

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

< p >: < br > ——http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/ < / p >

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

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

静态分析可以很好,但是真正可靠的静态分析通常需要程序员严格的工作。这里有一个成本效益计算,并且需要为导致编译时错误的检查设置较高的门槛。如果IDE承担通信方法可能抛出哪些异常(包括哪些是不可避免的)的角色,将会更有帮助。尽管如果没有强制的异常声明,它可能不会那么可靠,但大多数异常仍然会在文档中声明,IDE警告的可靠性并不是那么重要。

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

通过类Exception可以想象?不,因为__abc0是异常,同样异常也是__abc0,除了那些不< em > < / em > __abc0的异常,因为其他异常实际上是__abc10,这是另一种异常,一种异常异常,除非它发生,否则永远不应该发生,除非有时你必须捕捉它。这还不是全部,因为你还可以定义其他既不是__abc0也不是__abc4的异常,而仅仅是Exception1异常。

哪些是“已检查”异常?__abc0是受控异常,除非它们也是__abc1,是未检查的异常,然后是__abc2,也是__abc0,是受控异常的主要类型,但也有一个例外,那就是它们也是__abc5,因为那是另一种未检查的异常。

__abc是干什么用的?正如名字所暗示的,它们是异常,就像所有的__abc1一样,它们发生在运行时,实际上像所有的异常一样,除了__abc0与其他运行时__abc1相比是异常的,因为它们不应该发生,除非你犯了一些愚蠢的错误,尽管__abc0从来不是__abc5,所以它们是针对异常错误但实际上不是__abc5的事情。除了Exception2,它实际上是__abc5的RuntimeException。但所有的例外不都应该代表错误的情况吗?是的,都是。除了Exception3之外,这是一个非常普通的例外,因为文档解释说这是“正常发生”,这就是为什么他们把它作为Error的类型。

无论如何,由于我们将所有异常从中间分为__abc0(用于异常执行异常,因此不检查)和__abc1(用于不太异常的执行错误,因此检查,除非它们没有检查),现在我们需要两个不同类型的异常。所以我们需要Exception2和Exception3, Exception4和Exception5, Exception6和Exception7, Exception8和Exception9, IllegalAccessError0和IllegalAccessError1。

只不过,即使检查了异常,也总有(相当简单的)方法可以欺骗编译器,在不检查的情况下抛出异常。如果你这样做,你可能会得到UndeclaredThrowableException,除非在其他情况下,它可能抛出UnexpectedException,或UnknownException(与UnexpectedException0无关,仅用于“严重异常”),或UnexpectedException1,或UnexpectedException2,或UnexpectedException3。

哦,我们一定不能忘记Java 8时髦的新UncheckedIOException,这是一个RuntimeException异常,旨在通过包装由I/O错误(不会导致IOError异常,尽管也存在)引起的已检查的IOException异常,让你把异常检查的概念扔到窗口外,这些异常非常难以处理,所以你需要它们不被检查。

由于Java !

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

假设你定义了一个MyAppException extends Exception。它是由应用程序抛出的所有异常继承的顶级异常。在某些地方,你不想对特定的异常做出反应,你希望调用者来解决它,所以你声明throws MyAppException

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

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

罗伯特·c·马丁也不建议在他的书干净代码中使用受控异常,并认为受控异常违反了原则:

< p >什么价格?受控异常的价格是Open/Closed Principle1侵犯。如果从方法抛出checked异常 在您的代码中,并且catch高于三级,您必须声明 实例之间的每个方法签名中的异常 抓住。这意味着在软件的低级别上的更改可以 在许多更高级别上强制更改签名。变更的模块 必须重建和重新部署,即使他们不关心 改变。< / p >