针对检查异常的情况


456

多年以来,我一直无法获得以下问题的正确答案:为什么有些开发人员反对检查异常?我进行了无数对话,阅读了博客上的东西,阅读了布鲁斯·埃克尔(Bruce Eckel)所说的话(我看到的第一个反对他们的人)。

我目前正在编写一些新代码,并非常注意我如何处理异常。我正在尝试查看“我们不喜欢检查的异常”人群的观点,但我仍然看不到它。

我的每一次对话都以相同的问题为结尾而结束...让我进行设置:

总体而言(根据Java的设计方式),

  • Error 用于永远不应该抓到的东西(VM有花生过敏症,有人在上面撒了一罐花生)
  • RuntimeException 用于程序员做错了的事情(程序员走出了数组的结尾)
  • Exception(除外RuntimeException)用于程序员无法控制的事情(写入文件系统时磁盘已满,已达到该进程的文件句柄限制,并且您无法打开任何其他文件)
  • Throwable 只是所有异常类型的父级。

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

我听到的另一个常见论点是,检查异常会使重构代码更加困难。

对于“我要做的就是退出”参数,我说即使您要退出,也需要显示一条合理的错误消息。如果您只是在处理错误,那么当程序退出时如果没有明确说明原因的话,您的用户将不会过分高兴。

对于“它很难重构”人群,这表明未选择适当的抽象级别。与其声明方法不会引发IOException,不IOException应该将转换为更适合发生的异常。

我在将Main换成catch(Exception)(或在某些情况下catch(Throwable)确保程序可以正常退出)时没有问题-但我始终会捕获所需的特定异常。这样做至少可以显示一个适当的异常。错误信息。

人们从不回覆的问题是:

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

如果答案是Exception问题,那么您也将以与系统异常相同的方式处理程序员错误。对我来说这似乎是错误的。

如果您发现了问题,Throwable那么您将以相同的方式处理系统异常和VM错误(等等)。对我来说这似乎是错误的。

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

我会说显示堆栈跟踪的程序是错误的。不喜欢检查异常的人会不会有这种感觉?

因此,如果您不喜欢检查异常,是否可以解释为什么不这样做并回答未得到回答的问题?

编辑:我不是在寻找关于何时使用任何一种模型的建议,我正在寻找的是人们为什么从中扩展,RuntimeException因为他们不喜欢从中扩展Exception和/或为什么他们捕获异常,然后重新抛出RuntimeException而不是向其添加抛出他们的方法。我想了解不喜欢检查异常的动机。


46
我认为这不是完全主观的-这是一种旨在具有特定用途的语言功能,而不是让每个人都自行决定它的用途。它并不是特别有争议,它预先解决了人们很容易想到的特定反驳。
加雷斯2009年

6
来吧。作为一种语言功能,该主题已经并且可以以客观的方式得到解决。
Kurt Schelfthout,2009年

6
如果我有答案,@ cletus会“回答您自己的问题”!
TofuBeer

8
好问题。在C ++中,根本没有检查过的异常,我认为它使异常功能无法使用。最终,您不得不陷入每次调用的麻烦,因为您只是不知道它是否会抛出异常。
Dimitri C.

4
对于检查异常,我知道的最强论据是它们最初不在Java中出现,并且在引入它们时发现了JDK中的数百个bug。这有点早于Java 1.0。我个人不会没有他们,并且在这一点上与布鲁斯·埃克尔(Bruce Eckel)和其他人激烈地不同意。
罗恩侯爵,

Answers:


276

我想我读过的布鲁斯·埃克尔(Bruce Eckel)采访内容与我一样-一直困扰着我。实际上,争论是由受访者提出的(如果这确实是您正在谈论的职位)Anders Hejlsberg,.NET和C#的MS天才。

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

范虽然我是海斯伯格及其工作人员,但这种说法一直使我感到虚假。基本上可以归结为:

“受检查的异常是不好的,因为程序员只是通过始终捕获它们并将其关闭而滥用它们,这导致隐藏和忽略的问题,否则这些问题将被呈现给用户。”

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

论点摘要的摘要是“程序员不会正确使用它们,而没有正确使用它们比没有它们更糟糕”

这个论点有些道理,实际上,我怀疑高斯林不将运算符覆盖在Java中的动机来自一个类似的论点-它们使程序员感到困惑,因为它们经常被滥用。

但最后,我发现这是对海斯伯格的虚假论证,可能是后来提出的一种论证,用以解释这种缺乏而不是经过深思熟虑的决定。

我认为,尽管过度使用检查异常是一件坏事,并且容易导致用户草率处理,但正确使用它们会使API程序员为API客户端程序员带来巨大的好处。

现在,API程序员必须要小心,不要到处都抛出受检查的异常,否则它们只会惹恼客户程序员。(Exception) {}当Hejlsberg警告时,非常懒惰的客户端程序员将求助于追赶者,所有利益都将丢失,随之而来的是地狱。但是在某些情况下,无法替代良好的检查异常。

对我而言,经典示例是文件打开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)必须以字符串形式传递文件名
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的惰性程序员只是添加空的catch。发生这种情况时,即使对测试人员或客户端开发人员进行调试,非法的行值也不会很明显,而是会导致连锁错误,而这些错误很难确定其来源。阿丽亚娜火箭将在发射后炸毁。

好的,这就是问题所在:我说的是,检查异常FileNotFoundException不仅是一件好事,而且还是API程序员工具箱中用于以对客户端程序员最有用的方式定义API的基本工具。但是,CheckedInvalidRowNumberException这带来了很大的不便,导致编程错误,应该避免。但是如何区分。

我想这不是一门精确的科学,我想这是海斯伯格论点的基础,也许在一定程度上是合理的。但是我不乐意在这里把婴儿和洗澡水一起扔出去,所以让我在这里提取一些规则,以区分好检查的异常和坏的:

  1. 在客户无法控制或封闭与开放之间:

    仅当错误情况不受API 客户端编程器的控制时,才应使用检查的异常。这与系统的打开关闭程度有关。在受限的 UI中,客户端程序员可以控制所有从表视图添加和删除行的按钮,键盘命令等(封闭系统),如果尝试从中获取数据,则是客户端编程错误。不存在的行。在任何数量的用户/应用程序都可以添加和删除文件的基于文件的操作系统(开放系统)中,可以想象到客户端请求的文件已被删除而没有他们的知识,因此应该期望他们处理。

  2. 无处不在:

    客户端频繁进行的API调用上不应使用已检查的异常。通常,我指的是客户端代码中的很多地方-时间不多。因此,客户端代码通常不会尝试大量打开同一文件,但是我的表视图可以RowData通过不同的方法遍地开花。特别是,我将要编写很多代码,例如

    if (model.getRowData().getCell(0).isEmpty())

    每次都必须尝试/捕获,这会很痛苦。

  3. 通知用户:

    在您可以想象向终端用户显示有用的错误消息的情况下,应使用检查异常。这就是“当发生这种情况时您会怎么做?” 我在上面提出的问题。它还与第1项相关。由于您可以预测到客户端API系统外部的某些内容可能会导致文件不存在,因此您可以合理地告知用户:

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

    由于您的非法行号是由内部错误引起的,并且没有用户的过错,因此实际上没有任何有用的信息可提供给他们。如果您的应用程序不允许运行时异常进入控制台,则可能最终会给他们一些难看的消息,例如:

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

    简而言之,如果您认为客户程序员不能以对用户有用的方式解释异常,那么您可能不应该使用检查异常。

这就是我的规则。人为地作了准备,毫无疑问会有例外(如果可以,请帮助我完善它们)。但是我的主要论点是,在某些情况下,诸如FileNotFoundExceptionAPI契约的一部分所检查的异常与参数类型一样重要和有用。因此,我们不应仅仅因为滥用而放弃它。

抱歉,这并不意味着要花那么长时间。让我最后提出两个建议:

答:API程序员:谨慎使用受检查的异常以保留其有用性。如有疑问,请使用未经检查的异常。

B:客户程序员:养成在开发初期就创建包装异常(使用Google封装)的习惯。JDK 1.4和更高版本RuntimeException为此提供了一个构造函数,但是您也可以轻松创建自己的构造函数。这是构造函数:

public RuntimeException(Throwable cause)

然后养成这样的习惯:每当您必须处理受检查的异常并且感到懒惰(或者您认为API程序员首先使用受检查的异常过于热心)时,不要仅仅吞下该异常,将其包装起来然后扔掉

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

将其放入您的IDE的一个小代码模板中,并在感到懒惰时使用它。这样,如果您确实需要处理检查的异常,则在运行时看到问题后,您将不得不返回并处理该异常。因为,相信我(和Anders Hejlsberg),您永远都不会再回到您的TODO

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

110
实际上,打开文件是完全适得其反的检查异常的一个很好的例子。因为在大多数情况下,打开文件的代码不能对异常做任何有用的事情-最好在调用堆栈的多层上进行。被检查的异常迫使您使方法签名混乱,因此您无论如何都可以做-在最有意义的地方处理异常。
Michael Borgwardt,2009年

115
@Michael:同意您通常可以将这些IO异常处理几个级别以上-但是我没有听到您否认作为API客户端您必须预料到这些异常。因此,我认为检查异常是合适的。是的,您将必须在每个方法调用堆栈上声明“ throws”,但我不同意这很混乱。它是方法签名中有用的声明性信息。您是在说:我的低级方法可能会遇到丢失的文件,但它们将由您自己处理。我看不到任何损失,只有好的,干净的代码可以获得
大黄

23
@Rhubarb:+1,非常有趣的答案,显示了双方的论点。大。关于您的最后一条评论:请注意,不一定总是可以在调用堆栈上声明每个方法的抛出,尤其是在沿途实现接口的情况下。
R. Martinho Fernandes

8
这是一个非常有力的论据(我总体上同意),但是在某些情况下它是行不通的:通用回调。如果我有一些库调用某些用户定义的代码,则该库(我在Java中知道的)无法将已检查的异常从其调用的用户代码传播到调用它的用户代码。此问题还扩展到许多不支持正确的泛型类型的静态类型语言的类型系统的其他部分。提及此问题(可能是答复)会改善此答案。
Mankarse 2011年

7
@Rhubarb错误。受检查的异常旨在用于“突发事件”,可以并且应该对其进行处理,但始终与“提早投掷,追赶迟到”的最佳实践不兼容。打开文件是一个很好的例子,可以处理。大多数IOException(例如,写入字节),SQLException,RemoteException无法以有意义的方式处理。失败或重试应该处于“业务”或“请求”级别,而Java库中使用的检查异常是一个使错误变得很困难的错误。literatejava.com/exceptions/...
托马斯W ^

184

关于检查异常的事情是,按通常的理解,它们并不是真正的异常。相反,它们是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设计,也难怪受检查的异常会引起很多讨厌。

另一个特别不利的影响是控制反转,其中组件A向通用组件B提供了回调。组件A希望能够让异常从其回调引发回到其调用组件B的位置,但不能因为这将更改由B固定的回调接口。A只能通过将实际异常包装在RuntimeException中来实现,这是要编写的更多的异常处理样板。

Java及其标准库中实现的检查异常表示样板,样板,样板。用已经很冗长的语言来说,这不是胜利。


14
在您的代码示例中,最好将异常链接起来,以便在读取日志时可以找到原始原因:throw CanNeverHappenException(e);
Esko Luontola,2009年

5
我不同意。异常(是否经过检查)是特殊情况。示例:一种通过HTTP检索对象的方法。返回值是对象,否则为空,所有可能变坏的事物都是例外。像在C语言中一样将它们视为返回值只会导致混乱和不良的设计。
史密斯先生

15
@Mister:我的意思是,实际上,用Java实现的检查异常在行为上更像C中的返回值,而不是我们从C ++和其他Java以前的语言中可以识别的传统“异常”。IMO确实确实导致混乱和设计不佳。
bobince 2011年

9
同意标准库滥用检查异常肯定会增加混乱和不良的捕获行为。而且,通常只是由于文档不完善,例如,当出现“其他一些I / O错误”时,诸如Disconnect()之类的拆卸方法会引发IOException。好吧,我正在断开连接!我要泄漏句柄或其他资源吗?我需要重试吗?不知道为什么会发生这种情况,我无法得出应该采取的行动,因此我不得不猜测是应该吞咽,重试还是保释。
charstar 2011年

14
为“ API备用返回值” +1。查看检查的异常的有趣方式。
ZsoltTörök

75

与其重新提出针对检查异常的所有(许多)原​​因,不如我只选择一个。我记不清编写此代码块的次数:

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

99%的时间我对此无能为力。最后,块执行任何必要的清理(或至少应该清理)。

我也记不清看到这个的次数了:

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

为什么?因为有人必须处理它并且很懒。错了吗 当然。会发生吗?绝对。如果这是未经检查的异常怎么办?该应用程序将刚刚终止(最好是吞下一个异常)。

然后,我们有了令人毛骨悚然的代码,该代码使用异常作为流控制的形式,就像java.text.Format一样。Bzzzt。错误。用户将“ abc”放入表单上的数字字段也不例外。

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


4
但是,如果可以正确捕获异常,则可以通知用户,执行其他任务(登录?)并以受控方式退出应用程序。我同意某些API部分本可以设计得更好。出于懒惰的程序员的原因,我认为,作为一名程序员,您对代码负有100%的责任。
史密斯先生

3
请注意,try-catch-rethrow允许您指定一条消息-我通常使用它来添加有关状态变量内容的信息。IOExceptions的一个常见示例是添加相关文件的absolutePathName()。
托尔比约恩Ravn的安徒生

15
我认为,像Eclipse这样的IDE应当为您看到空的catch块的次数造成很多责备。确实,它们应该默认情况下重新抛出。
artbristol

12
“ 99%的时间我什么都做不了”-错误,您可以向用户显示一条消息“无法连接到服务器”或“ IO设备出现故障”,而不仅仅是让应用程序崩溃由于有点网络打ic。您的两个示例都是不良程序员的杰作。您应该攻击不良的程序员,而不要自己检查异常。就像我在将胰岛素用作色拉调味品时攻击胰岛素无助于治疗糖尿病一样。
AxiomaticNexus 2014年

2
@YasmaniLlanes您不能总是做这些事情。有时您需要遵循一个界面。当您设计良好的可维护API时尤其如此,因为您不能仅仅开始到处抛出副作用。两者及其带来的复杂性都会严重地困扰您。因此,是的,在99%的时间中,无需执行任何操作。
MasterMastic 2015年

50

我知道这是一个老问题,但是我花了一段时间与受检查的异常搏斗,还需要补充一些内容。请原谅我的时间!

我的主要牛肉(经过检查的例外)是它们破坏了多态性。要使它们在多态接口中表现出色是不可能的。

使用良好的Java List接口。我们有常见的内存实现,例如ArrayListLinkedList。我们还有skeletal类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。使用它的代码既不知道也不关心“控件”是当场组成,来自数组还是从文件,数据库,网络或将来的子空间中继中读取。由于该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为这种情况添加了一个类:用于IOException跨多态方法边界捕获和重新抛出s。有点证明了我的意思!)

因此,在这种情况下,检查异常根本不起作用。你不能扔他们。同理是Map由数据库支持的聪明方法,或者是java.util.Random通过COM端口连接到量子熵源的实现。一旦尝试使用多态接口实现新颖的工作,检查异常的概念就会失效。但是受检查的异常是如此隐蔽,以至于它们仍然无法使您安心,因为您仍然必须从低级方法中捕获并抛出任何异常,使代码混乱,并使堆栈跟踪混乱。

我发现无处不在的Runnable接口通常会退回到这个角落,如果它调用引发检查异常的东西。它不能按原样抛出异常,因此它所能做的就是通过捕获并重新抛出as来使代码混乱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方法不能导致IOExceptions,因此它们没有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);
}

像往常一样,检查异常会成为障碍。如果将变量声明为具有更多开放式异常要求的基本类型,则即使您知道它们不会在应用程序中发生,也必须为这些异常添加处理程序。

但是,等等,检查异常实际上非常烦人,以至于它们甚至都不会让您做相反的事情!想象一下,您当前正在捕获对调用所IOException引发的所有异常,但是您想将变量的声明类型更改为,则编译器会因尝试捕获无法抛出的已检查异常而感到愤怒。writeOutputStreamByteArrayOutputStream

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

在编辑和调试期间,该规则也很烦人。例如,有时我会暂时注释掉一个方法调用,如果它可能引发一个检查异常,则编译器现在将抱怨本地trycatch块的存在。因此,我也必须将其注释掉,现在在编辑其中的代码时,IDE将缩进错误的级别,因为{}被注释掉了。加!这是一个很小的投诉,但似乎检查过的异常唯一要做的就是引起麻烦。


我快完成了 对于受检查的异常,我最后的不满是,在大多数呼叫站点中,您无法执行任何操作。理想情况下,当出现问题时,我们将有一个称职的特定于应用程序的处理程序,该处理程序可以将问题告知用户和/或适当地结束或重试该操作。只有位于栈顶的处理程序才能执行此操作,因为它是唯一了解总体目标的程序。

取而代之的是,我们得到以下惯用语,它是关闭编译器的一种流行方式:

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

在GUI或自动程序中,看不到打印的消息。更糟糕的是,在出现异常后,它会与其余代码一起工作。异常实际上不是错误吗?然后不要打印它。否则,一会儿就会有其他事情发生,到那时原始的异常对象将消失。这个成语并不比BASIC的好On Error Resume Next或PHPerror_reporting(0);

调用某种记录器类并不是更好:

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

就像懒惰一样 e.printStackTrace();与不确定状态下的代码并且仍在继续努力。另外,选择特定的日志记录系统或其他处理程序是特定于应用程序的,因此这会损害代码重用。

可是等等!有一种简单通用的方法来查找特定于应用程序的处理程序。它在调用堆栈上更高(或被设置为Thread的未捕获异常处理程序)。因此,在大多数情况下,您要做的就是将异常抛出到堆栈的更高位置。例如throw e;。受检查的异常会妨碍您的工作。

我敢肯定,在设计语言时,检查异常听起来是个好主意,但实际上,我发现它们很麻烦,也没有任何好处。


对于使用WidgetList的size方法,我会将大小缓存在变量中,然后在构造函数中进行设置。构造函数可以随意抛出异常。如果在使用WidgetList时文件发生了更改,则此方法将无效,否则可能会很糟糕。
TofuBeer

2
SomeStupidExceptionOmgWho很好地照顾了有人将其抛出。因此,它永远都不应该被抛出(不良设计),或者您应该真正处理它。不幸的是,在1.0版之前的类(字节数组输出流)的实现不正确的情况下,设计也是如此。
TofuBeer

2
正确的习惯用法是一个指令,该指令将捕获嵌套子例程调用引发的任何指定异常,并将它们重新包装在RuntimeException。请注意,例程可以同时声明为,throws IOException并且还可以指定IOException从嵌套调用引发的所有异常均应视为未预期的和已包装的。
2013年

15
我是一位具有Java经验的专业C#开发人员,偶然发现了这篇文章。我对为什么有人会支持这种奇怪行为感到困惑。在.NET中,如果我想捕获特定类型的异常,则可以捕获该异常。如果我只想让它丢在堆栈上,那无事可做。我希望Java不是那么古怪。:)
aikeru

关于“有时我会暂时注释掉一个方法调用”-我学会了使用if (false)它。它避免了throw子句问题,并且警告帮助我更快地返回。+++也就是说,我同意您编写的所有内容。已检查的异常具有某些价值,但与它们的成本相比,该价值可以忽略不计。几乎总是它们只是阻碍。
maaartinus

45

好吧,这与显示堆栈跟踪信息或无提示崩溃无关。这是关于能够在层之间传递错误的问题。

受检查的异常的问题在于它们鼓励人们吞下重要的细节(即异常类)。如果您选择不包含该详细信息,则必须在整个应用程序中不断添加throws声明。这意味着1)一种新的异常类型将影响许多函数签名,以及2)您可能会错过您实际上想要捕获的异常的特定实例(例如,您打开了将数据写入数据的函数的辅助文件)。辅助文件是可选文件,因此您可以忽略其错误,但是因为签名throws IOException,很容易忽略它。

我现在实际上正在应用程序中处理这种情况。我们将几乎所有异常打包为AppSpecificException。这使签名真的很干净,我们不必担心throws签名爆炸。

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

这就是为什么我非常喜欢python中的异常处理的原因。您只能捕获想要和/或可以处理的东西。其他一切都会冒出来,就好像您自己重新扔了一样(无论如何,您都已经做了)。

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

  1. 捕获并处理特定的异常。例如,这是为了实现重试逻辑。
  2. 捕获并抛出其他异常。这里发生的所有事情通常都是日志记录,并且通常是一条陈旧的消息,如“无法打开$ filename”。这些是您无能为力的错误;只有更高水平的人才足够应付。
  3. 捕获所有内容并显示错误消息。这通常是调度程序的根源,它所做的一切就是确保它可以通过非异常机制(弹出对话框,封送RPC错误对象等)将错误传达给调用方。

5
您可以制作AppSpecificException的特定子类,以允许分离,同时保留普通方法签名。
托尔比约恩Ravn的安徒生

1
项目2的另一个非常重要的补充是,它允许您将信息添加到捕获的异常中(例如,通过嵌套在RuntimeException中)。在堆栈跟踪中找不到文件名比隐藏在日志文件深处要好得多。
托尔比约恩Ravn的安徒生

1
基本上,您的论点是“管理异常很累,所以我宁愿不处理它”。当异常冒出来时,它失去了意义,上下文的创建实际上是无用的。作为API的设计者,您应该从合同上清楚地知道出问题时可以预期的结果,如果我的程序崩溃是因为我没有被告知该异常会“冒泡”,那么作为设计者的您就失败了,并且由于您的故障,我的系统不稳定。
Newtopian

4
那根本不是我要说的。您的最后一句话实际上与我一致。如果所有内容都包装在AppSpecificException中,那么它就不会冒泡(并且失去了意义/上下文),是的,没有通知API客户端-这正是经过检查的异常(就像在Java中一样) ,因为人们不想使用很多throws声明来处理函数。
理查德·勒瓦瑟

6
@Newtopian -异常只能在“业务”或“请求”级别进行处理。失败或以大粒度重试是有意义的,而不是针对每一个微小的潜在失败。因此,异常处理最佳实践被总结为“提早抛出,追赶迟到”。受检查的异常使得更难在正确的级别上管理可靠性,并鼓励大量错误编码的捕获块。 literatejava.com/exceptions/...
托马斯W ^

24

信噪比

首先,检查异常会降​​低代码的“信噪比”。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#中效果很好。

以前对此写过博客。


3
在示例中,Java和C#都只是在不处理异常的情况下传播异常(Java通过IllegalStateException)。不同之处在于您可能要处理FileNotFoundException,但处理InvocationTargetException或InterruptedException不太可能有用。
路加·奎纳内

3
并以C#方式知道如何发生I / O异常?而且我永远也不会从运行中抛出异常...我认为是在滥用异常处理。抱歉,对于您的那部分代码,我仍然可以看到您的看法。
TofuBeer 2009年

7
我们到了:-)因此,对于每个新版本的API,您都必须梳理所有调用,并寻找可能发生的任何新异常?公司API内部很容易发生这种情况,因为它们不必担心向后兼容性。
TofuBeer

3
您是不是要降低信噪比?
neo2862

3
@TofuBeer在底层API的接口更改后,不是被迫更新代码吗?如果您那里只有未经检查的异常,那么您最终会发现程序不完整/不完整。
弗朗索瓦·布尔乔亚

23

Artima 对.NET的一位架构师Anders Hejlsberg进行了一次采访采访内容涵盖了反对检查异常的论点。简短的品尝者:

throws子句(至少是用Java实现的方法)不一定会迫使您处理异常,但是如果您不处理异常,它将迫使您确切地确认可能会通过哪些异常。它要求您要么捕获已声明的异常,要么将其放入自己的throws子句中。要解决此要求,人们会做一些荒谬的事情。例如,它们用“引发异常”修饰每个方法。这完全破坏了该功能,并且您使程序员编写了更多乱七八糟的东西。那对任何人都没有帮助。


2
其实是首席架构师。
陷阱

18
我读过,对我来说,他的论点可以归结为“那里有糟糕的程序员”。
TofuBeer

6
豆腐啤酒,一点也不。重点是,很多时候您都不知道如何处理被调用方法引发的Exception,甚至没有提到您真正感兴趣的情况。打开文件,例如,出现IO异常...这不是我的问题,因此我将其抛出。但是顶级调用方法只是想停止处理并通知用户存在未知问题。选中的异常根本没有帮助。这是一百万种可能发生的奇怪事情之一。
丹·罗森斯塔克

4
@yar,如果您不喜欢检查的异常,请执行“抛出新的RuntimeException(“,我们在执行Foo.bar()时没想到会发生此事,e)”并完成它。
托尔比约恩Ravn的安徒生

4
@ThorbjørnRavnAndersen:.net不幸地被复制了,这是Java的一个基本设计弱点,它使用异常的类型作为决定是否应该对异常采取行动的主要手段,以及指示事物的一般类型的主要手段其实,这两个问题在很大程度上是正交的。重要的不是出了什么问题,而是状态对象所在的状态。此外,.net和Java都默认假定作用和解决异常通常是同一件事,而实际上它们通常是不同的。
2012年

20

我一开始就同意您的观点,因为我一直都支持检查异常,并开始思考为什么我不喜欢在.Net中不检查异常。但是后来我意识到我并没有像检查异常那样实际。

要回答您的问题,是的,我喜欢我的程序显示堆栈跟踪,最好是显示丑陋的跟踪。我希望应用程序爆炸成您可能想要看到的最丑陋的错误消息的可怕堆。

原因是,如果这样做,我必须对其进行修复,并且必须立即对其进行修复。我想立即知道有问题。

您实际处理过多少次异常?我不是在说捕获异常-我是在讨论处理它们?编写以下内容太容易了:

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

而且我知道您可以说这是一种不好的做法,“答案”是做一些例外处理(让我猜,记录下来?),但是在现实世界(tm)中,大多数程序员只是不这样做它。

因此,是的,我不想捕获异常,如果我不必这样做,那么我希望我的程序在崩溃时会异常崩溃。默默地失败是最糟糕的结果。


Java鼓励您做这种事情,这样您就不必在每种方法签名中添加每种类型的异常。
yfeldblum

17
自从我正确地接受了检查的异常并适当地使用它们以来,我的程序就停止了运转,并在面对客户的不满中冒出了巨大的热情。如果在开发过程中有大量难看的错误堆栈跟踪,那么客户也一定会得到它们。D'love看到他的脸,当他看到ArrayIndexOutOfBoundsException并在崩溃的系统上有一英里高的堆栈跟踪,而不是一个小的托盘通知时说,无法解析按钮XYZ的颜色配置,因此默认使用了该软件,愉快地哼哼沿
Newtopian

1
Java可能需要的是“ cantHandle”声明,该声明将指定方法或try / catch代码块未准备好处理其中发生的特定异常,并且通过非显式方式发生的任何此类异常在该方法中抛出的异常(与调用方法相反)应自动包装并在RuntimeException中重新抛出。恕我直言,检查后的异常很少应该在不被包装的情况下向上传播。
超级猫

3
@Newtopian-我编写服务器和高可靠性软件,并且已经这样做了25年。我的程序从来没有崩溃过,我使用高可用性,重试和重新连接,基于集成的金融和军事系统。我有绝对的客观基础,喜欢运行时异常。受检查的异常使遵循正确的“早扔早抓”最佳实践变得更加困难。正确的可靠性和错误处理处于“业务”,“连接”或“请求”级别。(或者在解析数据时偶尔)。已检查的异常会妨碍正确执行。
Thomas W

1
您在这里谈论的异常RuntimeExceptions确实是您不必捕获的,我同意您应该让程序崩溃。您应始终捕获和处理的异常是已检查的异常,例如IOException。如果得到IOException,则代码中没有任何要修复的内容;您的程序不应仅仅因为存在网络故障而崩溃。
AxiomaticNexus

20

文章有效的Java异常很好地解释了何时使用未检查的以及何时使用检查的异常。以下是该文章的一些引言,以突出重点:

不可预见性:一种期望条件,要求该方法产生替代响应,并且可以用该方法的预期目的表示。该方法的调用者期望这些条件,并具有应对这些条件的策略。

错误: 计划外的条件,阻止方法达到其预期目的,如果不参考该方法的内部实现就无法描述该方法。

(因此不允许使用表格,因此您可能需要从原始页面阅读以下内容...)

偶然性

  • 被认为是:设计的一部分
  • 预计会发生:定期但很少
  • 谁在乎:调用该方法的上游代码
  • 示例:替代返回模式
  • 最佳映射:已检查的异常

故障

  • 被认为是:令人讨厌的惊喜
  • 预计会发生:从不
  • 谁在乎:需要解决问题的人
  • 示例:编程错误,硬件故障,配置错误,文件丢失,服务器不可用
  • 最佳映射:未经检查的异常

我知道何时使用它们,我想知道为什么不遵循该建议的人...不遵循该建议:-)
TofuBeer 2009年

什么是编程错误,如何与使用错误区分开?如果用户将错误的参数传递给程序,是否是编程错误?从Java的角度来看,它可能不是编程错误,但是从Shell脚本的角度来看,它是编程错误。那么什么是无效参数args[]呢?他们是偶然事件还是过错?
2013年

3
@TofuBeer-因为Java库设计者选择将所有不可恢复的低级故障作为已检查的异常(当显然应该取消检查它们时)。例如,FileNotFound是唯一应检查的IOException。关于JDBC-仅可连接到数据库,可以合理地认为是偶发事件。所有其他SQLExceptions应该已经失败并且未被选中。错误处理应正确地处于“业务”或“请求”级别-请参阅“提早投入,追赶迟到”的最佳实践。受检查的异常是这样做的障碍。
Thomas W

1
你的论点有一个巨大的缺陷。“偶然性”一定不能通过异常来处理,而应该通过业务代码和方法返回值来处理。顾名思义,例外是指特殊情况,因此是错误。
Matteo Mosca 2014年

@MatteoMosca错误返回码往往被忽略,这足以使它们失去资格。实际上,任何异常情况通常只能在堆栈中的某个地方进行处理,这是例外的用例。我可以想像File#openInputStream返回一样的东西Either<InputStream, Problem>-如果这就是您的意思,那么我们可能会同意。
maaartinus

19

简而言之:

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

检查异常的参数:

要了解为什么检查的异常可能不是一件好事,让我们转过一个问题,然后问:何时或为什么检查的异常具有吸引力,即为什么要让编译器强制执行异常声明?

答案很明显:有时您需要捕获一个异常,只有在被调用的代码为您感兴趣的错误提供特定的异常类时,才有可能。

因此,争论 checked异常是编译器迫使程序员宣布其抛出异常,并希望程序员也就那么也文档特定的异常类,并导致他们的错误。

但是实际上,包常常com.acme只抛出一个AcmeException而不是特定的子类。然后,呼叫者需要处理,声明或重新发出信号AcmeExceptions,但仍不能确定是AcmeFileNotFoundError发生了还是AcmePermissionDeniedError

因此,如果您仅对感兴趣AcmeFileNotFoundError,则解决方案是向ACME程序员提出功能请求,并告诉他们实现,声明和记录的子类AcmeException

那为什么要打扰呢?

因此,即使存在检查到的异常,编译器也不能强制程序员抛出有用的异常。仍然只是API质量的问题。

结果,没有检查异常的语言通常不会变得更糟。程序员可能很想抛出通用Error类而不是的非特定实例AcmeException,但是如果他们完全关心自己的API质量,那么他们将学习引入“ AcmeFileNotFoundError毕竟”。

总体而言,例外的说明和文档与普通方法的说明和文档没有太大区别。这些也是API设计的问题,如果程序员忘记实现或导出有用的功能,则需要改进API,以便您可以有效地使用它。

如果遵循这种推理方式,那么显而易见的是,声明,捕获和重新抛出异常的“烦恼”在像Java这样的语言中很常见,通常没有什么价值。

还值得一提的是,Java虚拟机并没有检查了例外-只是Java编译器检查它们,类文件以改变异常声明是在运行时兼容。受检查的异常不会仅通过编码样式来提高Java VM的安全性。


4
你的论点自相矛盾。如果“有时您需要捕获异常”并且API质量通常很差,而没有经过检查的异常,则您将不知道设计者是否忽略了记录某种方法抛出需要捕获的异常的文档。再加上投掷AcmeException而不是AcmeFileNotFoundError好运,就可以弄清楚您做错了什么以及需要在哪里抓住它。受检查的异常为程序员提供了针对不良API设计的少量保护。
伊娃

1
Java库设计犯了严重错误。“已检查的异常”是指可预测和可恢复的突发事件-例如找不到文件,连接失败。它们从不意味着或不适合进行低级系统性故障。强制打开要检查的文件是可以的,但是没有明智的重试或恢复操作,无法写入单个字节/执行SQL查询等。重试或恢复操作可以在“业务”或“请求”处正确处理。 ”级别,即检查异常会变得毫无意义。literatejava.com/exceptions/...
托马斯w ^

17

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

到目前为止,我发现使用Checked Exceptions处理代码库更加容易。当我使用其他人的API时,很高兴能确切看到我在调用代码并正确处理它们(记录,显示或忽略)时会遇到什么样的错误情况(是的,有一些有效的情况可以忽略异常,例如ClassLoader实现)。这为我编写的代码提供了恢复的机会。我会传播所有运行时异常,直到它们被缓存并使用一些通用的错误处理代码进行处理。当我发现确实不想在特定级别处理的检查异常,或者考虑到编程逻辑错误时,我将其包装到RuntimeException中并使其冒泡。永远不要在没有充分理由的情况下吞下异常(而这样做的充分理由非常稀缺)

当我使用未检查异常的代码库时,使我很难事先知道调用函数时会发生什么,这可能会严重破坏某些功能。

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

总而言之,我发现使用Checked Exceptions更容易,特别是在具有许多开发人员的大型项目中。


6
我愿意 对我来说,它们是合同的重要组成部分。不必在API文档中进行详细介绍,就可以快速了解最可能的错误情况。
韦恩·哈特曼

2
同意。当我尝试进行网络调用时,我曾经历过.Net中检查异常的必要性。知道随时可能发生网络故障,因此我必须通读API的整个文档以找出我需要专门针对那种情况的异常情况。如果C#检查了异常,我马上就会知道。其他C#开发人员可能只是让应用程序因简单的网络错误而崩溃。
AxiomaticNexus

13

例外类别

在谈论异常时,我总是参考Eric Lippert的Vexing异常博客文章。他将异常归入以下类别:

  • 致命 -这些异常不是您的错:您无法阻止这种情况,也无法明智地处理它们。例如,OutOfMemoryErrorThreadAbortException
  • 笨拙 -这些异常是您的错:您应该阻止它们,它们表示代码中的错误。例如ArrayIndexOutOfBoundsExceptionNullPointerException或任何IllegalArgumentException
  • 恼人的 -这些异常不是例外,不是您的错,您无法阻止它们,但必须处理它们。他们往往是一个不幸的设计决策的结果,如投掷NumberFormatException距离Integer.parseInt,而不是提供一个Integer.tryParseInt返回上解析失败一个布尔值false方法。
  • 外在的 -这些异常通常是例外,不是您的错,您不能(合理地)阻止它们,但必须处理它们。例如,FileNotFoundException

API用户:

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

检查异常

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

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

已检查的异常是必须处理的异常。处理异常就像吞下它一样简单。那里!处理异常。期。如果开发人员希望以这种方式处理它,那很好。但是他不能忽视这个例外,并受到警告。

API问题

但是,任何检查过烦人致命异常的API (例如JCL)都会给API用户带来不必要的压力。这些例外必须要处理,但任何的异常是很常见的,它不应该已经摆在首位的异常,或没有任何东西可以在搬运时完成。而使得Java开发者憎恨检查的异常。

另外,许多API没有适当的异常类层次结构,从而导致所有种类的非外部异常原因都由单个检查的异常类(例如IOException)表示。而且这也导致Java开发人员讨厌已检查的异常。

结论

外在异常是那些不是您的错,无法避免且应该处理的异常。它们构成了所有可能抛出的异常的一小部分。API应该仅具有检查的外部异常,而所有其他异常均未选中。这将使API更好,减轻API用户的负担,从而减少捕获所有,吞下或重新抛出未经检查的异常的需求。

因此,不要讨厌Java及其检查的异常。相反,讨厌过度使用已检查异常的API。


并且由于没有层次结构而滥用它们。
TofuBeer

1
FileNotFound和建立JDBC /网络连接是意外情况,可以纠正并检查异常,因为这些异常是可预测的(可能)可恢复的。其他大多数IOException,SQLException,RemoteException等都是不可预测和不可恢复的失败,应该是运行时异常。由于Java库设计错误,我们都被这个错误所迷惑,并且现在大多数人都使用Spring和Hibernate(他们的设计正确)。
Thomas W

尽管您可能不希望将其称为“处理”,但是您通常应该处理无脑的异常。例如,在Web服务器中,我将它们记录下来并向用户显示500。由于异常是意外的,因此在进行错误修复之前,我几乎可以做的所有事情。
maaartinus

6

好的...受检查的异常不是理想的情况,但有一些警告,但确实可以达到目的。创建API时,某些特定情况下的失败是该API的约定。当在诸如Java之类的高度静态类型化语言的上下文中,如果一个人不使用检查的异常,则必须依靠即席文档和约定来传达错误的可能性。这样做会消除编译器带来的处理错误的所有好处,而您完全会被程序员的善意所取代。

因此,一个删除Checked异常,例如在C#中进行的操作,那么一个人如何以编程和结构方式传达错误的可能性?如何通知客户代码这种错误可能发生并且必须加以解决?

在处理受检查的异常时,我听到了各种各样的恐惧,这是可以肯定的,但未经检查的异常也是如此。我说等几年,当API堆叠到很深的层次时,您会乞求某种结构化方法的回报来传达故障。

以这种情况为例,该异常被抛出到API层的底部某处,并且由于没有人知道甚至可能发生此错误而冒泡了,尽管这种错误在调用代码时是非常合理的扔了它(例如,FileNotFoundException而不是VogonsTrashingEarthExcept ...在这种情况下,是否处理它都没有关系,因为没有东西可处理了)。

许多人争辩说,无法加载文件几乎总是该过程的末日,它必定会导致可怕而痛苦的死亡。所以是的..确定...确定..您为某些内容构建了API,并在某个时刻加载了文件...作为该API的用户,我只能做出响应...“您到底该决定我的时间程序应该崩溃!” 当然可以,因为可以选择在其中吞噬异常并且不留任何痕迹,或者具有比Marianna沟槽更深的堆栈跟踪的EletroFlabbingChunkFluxManifoldChuggingException,我会毫不犹豫地选择后者,但这是否意味着这是处理异常的理想方法?我们可以不在中间的地方,在异常遍历到新的抽象级别时会重铸并包装该异常,以便它实际上意味着什么吗?

最后,我看到的大多数论据是“我不想处理异常,许多人不想处理异常。检查异常迫使我去处理它们,因此我讨厌检查异常”。将其释放到goto的鸿沟地狱只是愚蠢的,缺乏判断力和远见。

如果我们消除了检查异常,我们也可以消除函数的返回类型,并且总是返回“ anytype”变量……那会使生活变得如此简单,不是吗?


2
如果有一种声明性的方式说块中的任何方法调用均不会引发某些(或任何)受检查的异常,并且任何此类异常应自动包装并重新抛出,则检查的异常将很有用。如果对声明为引发已检查异常的方法的调用权衡了调用速度/返回的异常处理速度(这样,预期的异常可以几乎与正常程序流一样快地处理),则它们可能会更加有用。但是,这两种情况目前都不适用。
2014年

6

确实,一方面,检查异常会提高程序的健壮性和正确性(您必须对接口进行正确的声明-方法抛出的异常基本上是一种特殊的返回类型)。另一方面,您遇到的问题是,由于异常“冒泡”,因此,当您更改一个异常时,通常需要更改很多方法(所有调用方和调用方的调用方,依此类推)方法抛出。

Java中的检查异常不能解决后一个问题。C#和VB.NET用洗澡水把婴儿扔出去。

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

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

尽管这是一篇学术论文,但我还是鼓励您阅读(部分),因为它很好地解释了检查异常的好处和缺点。


5

这不是反对检查异常的纯概念的论据,但是Java用于它们的类层次结构表现得异常出色。我们总是将事物简称为“例外”,这是正确的,因为语言规范也将其称为“例外”但是在类型系统中如何命名和表示异常呢?

在课堂上,Exception人们会想象吗?好吧,不,因为Exceptions是异常,并且同样是Exceptions,除了不是 Exception s的那些异常之外,因为其他异常实际上是Errors,这是另一种异常,是一种例外情况异常,除了什么时候做,除了某些时候必须永远不要抓住。除此之外,这还不是全部,因为您还可以定义既不Exception是也不是s的其他异常,Error而仅仅是Throwable异常。

其中哪些是“已检查”异常?Throwables是已检查的异常,除非它们也是Errors,这是未检查的异常,然后是Exceptions,这也是Throwables,并且是已检查的异常的主要类型,但也有一个例外,那就是如果它们也是RuntimeExceptions,因为这是另一种未经检查的异常。

RuntimeExceptions是做什么用的?就像顾名思义,它们是异常,就像所有Exceptions一样,它们在运行时发生,就像所有异常一样,除了RuntimeExceptions与其他运行时Exceptions 相比是例外,因为它们不应该发生,当您犯一些愚蠢的错误时,尽管RuntimeExceptions永远不会Error是s,所以它们是针对那些异常错误但实际上并非Errors的东西。除了RuntimeErrorException,这真的是一个RuntimeExceptionError秒。但是,难道不是所有的异常都应该代表错误的情况吗?是的,所有人。除了ThreadDeath,这是一个非常异常的异常,因为文档解释说这是“正常现象”,并且Error

无论如何,由于我们将所有异常分为中间的Errors(用于例外执行异常,因此未经检查)和Exceptions(用于较少的例外执行错误,因此经过检查,除非它们不在时),我们现在需要两个每个例外都有不同的种类。所以我们需要IllegalAccessErrorand IllegalAccessExceptionInstantiationErrorand InstantiationExceptionand NoSuchFieldErrorand NoSuchFieldExceptionand NoSuchMethodErrorand NoSuchMethodExceptionand ZipErrorand and and and and ZipException

除了即使检查了异常,也总是有(相当容易)欺骗作弊程序并在不进行检查的情况下将其抛出的方法。如果你这样做你,你可能会得到一个UndeclaredThrowableException,但在其他情况下,它可能会扔了作为UnexpectedException,或UnknownException(这是无关的UnknownError,这是唯一的“严重异常”),或ExecutionException,或InvocationTargetException,或ExceptionInInitializerError

哦,我们一定不要忘记Java 8的时髦的new UncheckedIOException,它是一个RuntimeException异常,旨在让您通过包装IOException由I / O错误引起的已检查异常(IOError尽管存在,它不会引起异常)将异常检查概念丢到窗外也是如此),它们非常难于处理,因此您需要对其进行检查。

谢谢Java!


2
据我所知,该答案仅以讽刺,可以说是有趣的方式说“ Java的例外是一团糟”。似乎没有做的是解释为什么程序员倾向于避免尝试理解这些事情应该如何工作。同样,在现实生活中(至少我有机会应对),如果程序员不故意使生活变得更艰难,那么异常就不会像您所描述的那样复杂。
CptBartender

5

问题

我看到的异常处理机制最严重的问题是,它大规模引入了代码重复!坦白说:在大多数项目中,开发人员真正需要做的事情是在95%的时间内与用户进行某种形式的交流(在某些情况下,还与开发团队进行交流,例如通过发送e -带有堆栈跟踪的邮件)。因此,通常在处理异常的每个地方都使用相同的代码行/块。

假设我们在每个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注释)。

这样,我们就可以毫不费力地区分开发和发行版应用程序中的异常处理行为(例如,开发-记录错误并暂停应用程序,prod-记录错误以获取更多详细信息并让应用程序继续执行)。

最后一件事

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


6
发明了例外可以对它们做一些有用的事情。将它们写入日志文件或呈现漂亮的窗口是没有用的,因为此问题无法解决原始问题。做有用的事情需要尝试不同的解决方案策略。示例:如果无法从服务器AI获取数据,请在服务器B上进行尝试。或者,如果算法A产生堆溢出,请尝试算法B,它虽然慢得多,但可能成功。
2013年

2
@ceving是的,从理论上讲,这一切都是正确的。但是,现在让我们重新练习单词。请诚实地回答您在实词项目中执行的频率如何?catch在这个实际项目中,模块的哪一部分对exceptins确实“有用”?10%会很好。产生异常的常见问题类似于尝试从不存在的文件中读取配置,OutOfMemoryErrors,NullPointerExceptions,数据库约束完整性错误等。您是否真的要从所有这些文件中正常恢复?我不相信你:)。通常,没有办法恢复。
Piotr Sobczyk

3
@PiotrSobczyk:如果程序由于请求者请求而采取了某些措施,并且该操作以某种方式失败,并且没有破坏系统状态下的任何内容,那么通知用户该操作无法完成是非常有用的处理情况的方式。C#和.net中异常的最大失败之处在于,没有一致的方法来确定系统状态下的任何内容是否已损坏。
超级猫

4
正确,@ PiotrSobczyk。通常,对异常采取的唯一正确操作是回滚事务并返回错误响应。解决异常的想法意味着我们不(也不应该)拥有知识和权威,并且违反了封装。如果我们的应用程序不是数据库,则不应尝试修复数据库。干净地失败并避免写入错误的数据,足够有用
2014年

@PiotrSobczyk昨天,我通过故障转移至数据库的历史版本保证指向该对象的旧版本。
同名的2014年


4

我在c2.com上写的内容与原始形式大致相同CheckedExceptionsAreIncompatibleWithVisitorPattern

综上所述:

访客模式及其亲属是一类接口,其中间接调用方和接口实现都知道异常,但是接口和直接调用方形成了一个不知道的库。

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

在这种情况下,检查异常的最终结果是许多其他无用的代码,这些代码实际上在运行时消除了编译器的检查异常约束。

至于潜在的问题:

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


1
接口中应该有DAGNodeException之类的东西,然后捕获IOException并将其转换为DAGNodeException:public void call(DAGNode arg)抛出DAGNodeException;
TofuBeer

2
@TofuBeer,这正是我的观点。我发现不断包装和解开异常比删除已检查的异常更糟糕。
约书亚

2
好吧,那时我们完全不同意...但是您的文章仍然没有回答真正的根本问题,即当引发运行时异常时如何停止应用程序向用户显示堆栈跟踪。
TofuBeer

1
@TofuBeer-失败时,告诉用户失败是正确的!除了用“空”或不完整/不正确的数据“覆盖”故障之外,您还有什么选择?假装成功是一个谎言,这只会使事情变得更糟。凭借在高可靠性系统方面拥有25年的经验,重试逻辑只能在适当的地方谨慎使用。无论您重试多少次,我也希望访客可能再次失败。除非您要驾驶飞机,否则切换到相同算法的第二个版本是不切实际和不切实际的(并且可能仍然会失败)。
Thomas W

4

尝试仅解决未解决的问题:

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

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

例如:

如果我正在为数据库编写连接测试器,或者要检查用户输入的XPath的有效性的东西,那么我可能想捕获并报告该操作引发的所有已检查和未检查的异常。

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


2
究竟。异常处理的方式简单明了。如Dave所说,正确的异常处理通常是在较高的层次上进行的。“早扔,晚抓”是原则。受检查的异常使这变得困难。
Thomas W

4

本文是我读过的有关Java异常处理的最佳文章

它偏重于未检查的异常而不是检查的异常,但是这种选择会在很强的论点基础上进行透彻的解释。

我不想在这里引用过多的文章内容(最好是整体阅读),但是它涵盖了该线程中未经检查的异常拥护者的大部分论点。特别是涉及了以下论点(似乎很流行):

以这种情况为例,该异常被抛出到API层的底部某处,并且由于没有人知道甚至可能发生此错误而冒泡了,尽管这种错误在调用代码时是非常合理的扔了它(例如,FileNotFoundException而不是VogonsTrashingEarthExcept ...在这种情况下,是否处理它都没有关系,因为没有东西可处理了)。

作者“回应”:

假定不应捕获所有运行时异常并将其传播到应用程序的最顶端是绝对不正确的。(...)对于需要由系统/业务需求分别处理的每个特殊条件,程序员必须决定在哪里捕获该条件,一旦发现该条件,该怎么办。必须严格根据应用程序的实际需求执行此操作,而不是基于编译器警报。必须允许所有其他错误自由地传播到将被记录在最顶层的处理程序中,并采取适当的(也许是终止)操作。

主要思想或文章是:

对于软件中的错误处理,唯一可以做出的安全正确的假设是,绝对存在的每个子例程或模块都可能发生故障!

因此,如果“ 没有人知道甚至可能发生此错误 ”,则该项目存在问题。如作者建议的那样,此类异常至少应由最通用的异常处理程序(例如,处理所有未由更具体的处理程序处理的异常的处理程序)处理。

很遗憾,没有多少人会发现这篇伟大的文章:-(。我全心建议每个犹豫不决的人都花些时间阅读它。


4

受检查的异常以其原始形式是试图处理突发事件而不是失败。值得称赞的目标是突出显示特定的可预测点(无法连接,找不到文件等)并确保开发人员能够处理这些点。

最初的概念从未包含在内,是要强制宣布发生大量系统性和不可恢复的故障。这些失败永远都不是正确的,不能被宣布为检查异常。

通常,代码中可能会发生故障,而EJB,Web和Swing / AWT容器已经通过提供最外部的“失败请求”异常处理程序来解决此问题。最基本的正确策略是回滚事务并返回错误。

关键一点是运行时和检查的异常在功能上是等效的。没有已检查的异常可以执行的处理或恢复,而运行时异常则无法做到。

反对“检查”异常的最大论点是,大多数异常无法修复。一个简单的事实是,我们没有损坏的代码/子系统。我们看不到实现,我们对此不负责,也无法修复。

如果我们的应用程序不是数据库..我们不应该尝试修复数据库。那会违反 封装原理

尤其有问题的是JDBC(SQLException)和EJB的RMI(RemoteException)。这些强迫性普遍存在的,系统性的,实际上无法修复的可靠性问题并未得到广泛认可,而是按照原始的“受检查的例外”概念确定了可修复的突发事件。

Java设计中的另一个严重缺陷是,异常处理应正确地置于最高的“业务”或“请求”级别。这里的原则是“早扔,迟到”。受检查的异常作用不大,但会妨碍这种方式。

在Java中,我们存在一个明显的问题,即需要成千上万的虚假尝试捕获块,其中很大一部分(40%+)被错误编码。这些中几乎没有一个实现任何真正的处理或可靠性,但是会带来大量的编码开销。

最后,“检查异常”与FP功能编程几乎不兼容。

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

许多人都在谈论“处理”检查的异常,但是却在谈论。在失败后继续执行,如果数据为空,不完整或不正确以假装成功,则无法进行任何处理。这是最低形式的工程/可靠性不当行为。

彻底失败是处理异常的最基本的正确策略。回滚事务,记录错误并向用户报告“失败”响应是正确的做法-而且最重要的是,防止将不正确的业务数据提交到数据库。

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

最后,失败比使用不正确的数据运行要好得多。继续将导致次要错误,与原始原因相距较远且难以调试。否则最终会导致提交错误的数据。人们为此被解雇。

请参阅:
- http://literatejava.com/exceptions/checked-exceptions-javas-biggest-mistake/


1
我的观点是,适当失败是总的策略。未检查异常有助于避免强制插入捕获块。然后,可以将捕获和错误记录留给一些最外面的处理程序,而不是在整个代码库中将其错误编码数千次(这实际上是隐藏错误的原因)。对于任意故障,未经检查的异常绝对是最正确的。意外情况 -可预见的结果,例如资金不足-是唯一值得合法检查的例外情况。
Thomas W

1
我上面的答案已经解决了这个问题。首先,最重要的是,1)最外层的失败处理程序应该抓住一切。除此之外,仅对于特定的已识别站点,2)可以捕获并处理特定的预期突发事件-在直接站点处将其抛出。这意味着找不到文件,资金不足等可以从中追回-不会再多了。封装原理意味着外层不能/不应该负责理解/从内部深处的故障中恢复。第三,3)其他所有东西都应该向外扔-尽可能不检查。
Thomas W

1
最外面的处理程序捕获异常,将其记录下来,并返回“失败”响应或显示错误对话框。非常简单,一点也不难定义。关键是,由于封装原理,每个无法立即在本地恢复的异常都是无法恢复的故障。如果要了解的代码无法恢复,则请求总体上会正确无误地失败。这是正确执行此操作的正确方法。
Thomas W

1
不正确 最外层处理程序的工作是彻底清除故障并在“请求”边界记录错误。中断的请求将正常失败,报告异常,线程能够继续为下一个请求提供服务。这样的最外层处理程序是Tomcat,AWT,Spring,EJB容器和Java“主”线程中的标准功能。
Thomas W

1
为什么在请求边界或最外层的处理程序中报告“真正的bug”是危险的?我经常从事系统集成和可靠性方面的工作,其中正确的可靠性工程实际上很重要,并使用“未经检查的异常”方法来做到这一点。我不确定您实际上在争论什么-似乎您可能想以不受限制的例外方式实际花费3个月,对此有所了解,然后也许我们可以进一步讨论。谢谢。
Thomas W

4

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

事实是,如果您强制执行已检查的异常,那么您将无法迫使每个程序员“做正确的事”,而其他所有人现在都是附带的损害,他们只是讨厌您所制定的规则。

在那里修复错误的程序!不要试图修复不允许的语言!对于大多数人来说,“对异常做一些事情”实际上只是在告诉用户。我也可以向用户说明未检查的异常,因此请不要在我的API中使用已检查的异常类。


是的,我只是想强调无法到达的代码(会产生错误)与可预测结果的条件之间的区别。稍后我将删除此评论。
Holger

3

受检查的异常的问题在于,即使该接口的一种实现都使用该接口的方法,异常也经常附加到该接口的方法上。

检查异常的另一个问题是它们易于被滥用。完美的例子是in java.sql.Connectionclose()方法。SQLException即使您已经明确声明已完成Connection ,它也可能抛出。哪些信息可以close()传达您所关心的信息?

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

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

另外,不要让我开始使用各种解析方法和NumberFormatException ... .NET的TryParse(不会引发异常)非常容易使用,不得不回到Java的痛苦(我们同时使用Java和Java) C#)。

*作为附加说明,PooledConnection的Connection.close()甚至不会关闭连接,但是由于它是已检查的异常,因此您仍然必须捕获SQLException。


2
是的,任何驱动程序都可以...问题是“为什么程序员应该关心?” 因为他无论如何都要访问数据库。该文档甚至警告您在调用close()之前应始终commit()或rollback()当前事务。
Powerlord

许多人认为关闭文件不会引发异常... stackoverflow.com/questions/588546 / ... 您是否100%确定没有任何情况要紧?
TofuBeer

我100%确信没有任何情况会很重要,并且调用方不会进行try / catch。
马丁

1
紧密联系的典范,马丁!我只能重新表述一下:如果我们只是明确声明我们已经完成了连接,那么在关闭它时为什么要担心发生了什么。像这样的更多情况下,程序员并不真正在乎是否发生异常,而他对此是绝对正确的。
Piotr Sobczyk

1
@PiotrSobczyk:如果一些SQL驱动程序在启动事务后关闭连接,但既不确认也不回滚,则会发出尖叫声。恕我直言,至少在某些情况下,cause叫不会比忽略提示问题更好,因为叫不会导致其他异常丢失。
超级猫

3

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

繁琐的成本抵消了这种微小的好处(尤其是在较大的,不太灵活的代码库中,不断修改接口签名是不切实际的)。

静态分析可能很好,但是真正可靠的静态分析通常会僵硬地要求程序员进行严格的工作。有一个成本效益计算,需要将标尺设置得很高,以进行检查,从而导致编译时错误。如果IDE负责传达方法可能抛出哪些异常(包括不可避免的异常)的角色,则将更加有用。尽管没有强制的异常声明可能不会那么可靠,但是大多数异常仍将在文档中声明,并且IDE警告的可靠性不是那么关键。


2

我认为这是一个很好的问题,完全没有争议。我认为第三方库应该(通常)抛出未经检查的异常。这意味着您可以隔离对库的依赖(即,您不必重新抛出它们的异常或抛出Exception-通常是不好的做法)。Spring的DAO层就是一个很好的例子。

另一方面,通常应检查来自核心Java API的异常是否可以处理。拿FileNotFoundException或(我的最爱)InterruptedException。这些条件几乎总是应专门处理(即您对an InterruptedException的反应与对an的反应不同IllegalArgumentException)。检查您的异常的事实迫使开发人员考虑条件是否可处理。(也就是说,我很少看到InterruptedException处理得当!)

还有一件事-a RuntimeException并不总是“开发人员在哪里出错了”。当您尝试创建enumusing 时会抛出非法的参数异常,valueOf并且没有enum该名称。这不一定是开发人员的错误!


1
是的,这是开发人员的错误。他们显然没有使用正确的名称,因此他们必须返回并修复其代码。
AxiomaticNexus

@AxiomaticNexus没有理智的开发人员使用enum成员名称,仅仅是因为他们使用enum对象代替。因此,错误的名称只能来自外部,无论是导入文件还是其他名称。处理此类名称的一种可能方法是调用MyEnum#valueOf并捕获IAE。另一种方法是使用pre-filled Map<String, MyEnum>,但这是实现细节。
maaartinus

@maaartinus在某些情况下,使用枚举成员名称时字符串不会从外部传入。例如,当您要动态遍历所有成员以对每个成员执行某项操作时。此外,字符串是否来自外部无关紧要。开发人员拥有所需的所有信息,以了解在将x字符串传递给“ MyEnum#valueOf”之前是否会导致错误。无论如何,如果将x字符串传递给“ MyEnum#valueOf”会导致错误,那显然是开发人员的错误。
AxiomaticNexus

2

是反对检查异常的一种说法(来自joelonsoftware.com):

原因是我认为例外并不比自1960年代以来就被认为有害的“ goto”更好,因为它们会导致从一个代码点到另一个代码点的突然跳跃。实际上,它们比goto的情况严重得多:

  • 它们在源代码中不可见。查看代码块,包括可能会抛出异常或可能不会抛出异常的函数,无法查看可能会抛出异常以及从何处抛出异常。这意味着即使仔细的代码检查也不会发现潜在的错误。
  • 它们为一个函数创建了太多可能的出口点。要编写正确的代码,您实际上必须考虑函数中所有可能的代码路径。每次调用可能引发异常且未现场捕获的函数时,都会为由突然终止的函数,导致数据处于不一致状态的函数或其他您未曾遇到的代码路径引起的意外错误创造机会想一想。

3
+1您可能想在答案中总结论点吗?它们就像看不见的杂项和例程的提早退出,散布在整个程序中。
MarkJ

13
一般而言,这更多是针对异常的争论。
Ionuț G. Stan,

10
你真的读过这篇文章!首先,他通常讨论异常,其次,“它们在源代码中是不可见的”部分专门适用于UNCHECKED异常。这是检查异常的全部要点...,以便您知道什么代码将什么地方扔了
Newtopian

2
@Eva他们不一样。使用goto语句,您可以看到goto关键字。通过循环,您可以看到右括号或breakor continue关键字。所有这些都跳到当前方法中的某个点。但你不能总是看到throw,因为往往它不是在当前的方法,但是,它调用另一个方法(可能是间接的。)
finnw

5
@finnw函数本身就是goto的一种形式。您通常不知道您正在调用的功能正在调用什么功能。如果您编写的没有功能的程序,则看不见的异常不会有问题。这意味着问题不专门与异常相关,并且通常不是针对异常的有效论点。您可以说错误代码更快,可以说monad更干净,但是goto参数很愚蠢。
伊娃(Eva)2013年

2

不需要检查异常的充分证明是:

  1. 许多框架为Java做一些工作。像Spring将JDBC异常包装为未检查的异常一样,将消息抛出到日志中
  2. Java之后的许多语言,甚至是Java平台上的顶级语言-它们都不使用它们
  3. 检查异常,这是有关客户端如何使用引发异常的代码的一种预测。但是编写此代码的开发人员永远不会知道代码客户端正在从事的系统和业务。例如,一个Interfcace方法强制抛出已检查的异常。系统上有100种实现,其中50种甚至90种实现不会引发此异常,但是如果客户端用户引用该接口,则客户端仍必须捕获此异常。那些50或90个实现往往会自己处理这些异常,从而将异常放入日志中(这对他们来说是好习惯)。我们应该怎么做?我最好有一些可以完成所有工作的后台逻辑-将消息发送到日志。而且,如果我作为代码的客户,会觉得我需要处理异常-我会做的。
  4. 当我在Java中使用I / O时的另一个示例,如果文件不存在,它会强制我检查所有异常。我该怎么办?如果不存在,则系统将不会进行下一步。此方法的客户端不会从该文件中获取预期的内容-他可以处理运行时异常,否则我应该首先检查Checked Exception,将消息记录下来,然后从该方法中抛出异常。否...否-我最好使用RuntimeEception自动执行此操作,它会自动/自动点亮。手动处理没有任何意义-我很高兴在日志中看到一条错误消息(AOP可以帮助解决此问题。如果最终我认为系统应该向最终用户显示弹出消息-我将显示它,这不是问题。

如果Java 在使用I / O之类的核心库时能为我提供选择使用什么的选项,我感到很高兴。Like提供了两个相同类的副本-一个用RuntimeEception包装。然后我们可以比较人们会用什么。但是,到目前为止,许多人最好还是在Java或其他语言之上寻求一些框架。像Scala一样,JRuby也可以。许多人只是认为SUN是正确的。


1
而不是拥有两个版本的类,应该有一种简洁的方法来指定不希望代码块进行的任何方法调用都不会引发某些类型的异常,并且任何此类异常都应该通过某些指定的方式进行包装,并且重新抛出(默认情况下,RuntimeException使用适当的内部异常创建一个新的)。不幸的是,使外部方法throws成为内部方法的例外比让内部方法包装来自内部方法的例外更为简洁,因为后者的做法通常是正确的。
supercat

2

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

假设您定义了一个MyAppException extends Exception。它是应用程序引发的所有异常所继承的顶级异常。每个方法都声明throws MyAppException这有点麻烦,但可管理。异常处理程序记录异常并以某种方式通知用户。

在您要实现一个不是您自己的接口之前,一切看起来都不错。显然,它没有声明要抛出的意图MyApException,因此编译器不允许您从那里抛出异常。

但是,如果您的异常扩展了RuntimeException,则接口不会有问题。如果愿意,您可以在JavaDoc中自愿提及该异常。但是除此之外,它只是在所有内容中无声地冒出气泡,被捕获在异常处理层中。


1

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

这是Java专家关于何时使用检查的异常的另一种观点。他承认其他人提到的许多负面因素: 有效例外


2
Java中检查异常的问题源于更深层次的问题,即过多的信息被封装在异常的TYPE中,而不是实例的属性中。如果“被检查”是抛出/捕获站点的一种属性,并且可以以声明方式指定是否应将逃避代码块的已检查异常保留为已检查异常,还是将其视为已检查异常,则具有已检查异常将非常有用。通过任何封闭的块作为未检查的异常;同样,catch块应该能够指定它们仅需要检查的异常。
2012年

1
假设如果尝试访问不存在的键,则指定了字典查找例程以抛出某种特定类型的异常。客户端代码捕获此类异常可能是合理的。但是,如果查找例程使用的某些方法恰巧以查找例程所不希望的方式引发了相同类型的异常,则客户端代码可能不应捕获该异常。将检查作为异常实例,抛出站点和捕获站点的属性将避免此类问题。客户端将捕获该类型的“已检查”异常,从而规避意外的异常。
2012年

0

我已经阅读了很多有关异常处理的内容,即使(大部分时间)我不能真正地说我对检查异常的存在感到高兴还是难过,这是我的看法:低级代码(IO,网络)中的检查异常,操作系统等)以及高级API /应用程序级别的未经检查的异常。

即使它们之间的界线不是那么容易,我发现将多个API /库集成在同一个屋顶下而不包装所有时间的检查异常确实很烦人/困难,但是另一方面,有时有用/更好地被迫捕获某些异常并提供一种在当前上下文中更有意义的异常。

我正在从事的项目需要大量库并将它们集成在完全基于未检查异常的同一API下,该框架提供了一个高级API,该API最初充满了已检查异常并且只有几个未检查异常异常(初始化异常,ConfigurationException等),我必须说不是很友好。大多数时候,您不得不捕获或重新抛出不知道如何处理的异常,或者甚至不在乎(不要与您混淆应该忽略异常),尤其是在客户端,点击可能会引发10个可能的(已检查)异常。

当前版本(第3版)仅使用未经检查的异常,并且它具有全局异常处理程序,负责处理所有未捕获的异常。该API提供了一种注册异常处理程序的方法,该方法将确定是否将异常视为错误(大多数情况下是这种情况),即记录并通知某人,或者可以表示其他含义,例如该异常,AbortException,这意味着中断当前执行线程,并且不要记录任何错误,因为不希望这样做。当然,为了计算出所有自定义线程,必须使用try {...} catch(all)处理run()方法。

公共无效run(){

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

}

如果您使用WorkerService安排作业(可运行的,可调用的,工人的),该作业将为您处理所有事情,则这不是必需的。

当然,这只是我的观点,可能不是正确的观点,但这对我来说似乎是一种不错的方法。我会在发布项目后看到我认为对我有好处,对其他人也有好处... :)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.