多年来,我一直无法对以下问题给出一个合理的答案:为什么有些开发人员如此反对检查异常?我有过无数次的交谈,在博客上读东西,读布鲁斯·埃克尔必须说的话(我看到的第一个人公开反对他们)。
我目前正在编写一些新的代码,并非常注意如何处理异常。我试着从"我们不喜欢被检查的例外"人群的角度来看问题,但我还是看不到。
每一次谈话都以同样的问题结束,没有人回答…我来设置一下:
通常(从Java是如何设计的)
- 错误在于那些不应该被抓住的东西(VM对花生过敏,有人把一罐花生掉在上面)
- RuntimeException是针对程序员做错的事情(程序员离开了数组的末尾)
- 异常(runtimeexception除外)是指程序员无法控制的事情(写入文件系统时磁盘已满,已达到进程的文件句柄限制,无法再打开任何文件)
- throwable只是所有异常类型的父级。
我听到的一个常见的论点是,如果发生异常,那么开发人员要做的就是退出程序。
我听到的另一个常见的论点是,检查异常会使重构代码变得更加困难。
对于"我要做的就是退出"的论点,我说即使您退出,也需要显示一条合理的错误消息。如果您只是在处理错误上下赌注,那么当程序退出时,如果没有明确的指示原因,您的用户不会太高兴。
对于"它使重构变得困难"的人群来说,这意味着没有选择合适的抽象级别。与其声明方法抛出IOException,不如将IOException转换为更适合当前情况的异常。
我不存在用catch(exception)包装main的问题(或者在某些情况下,catch(throwable)以确保程序可以优雅地退出——但我总是捕获我需要的特定异常。这样做至少可以让我显示适当的错误消息。
人们从不回答的问题是:
If you throw RuntimeException
subclasses instead of Exception
subclasses then how do you know what
you are supposed to catch?
如果答案是catch exception,那么您也可以像处理系统异常一样处理程序员错误。我觉得这不对。
如果您捕获了可丢弃的,那么您将以相同的方式处理系统异常和VM错误(等等)。我觉得这不对。
如果答案是只捕获抛出的异常,那么如何知道抛出的异常是什么?当程序员X抛出一个新的异常而忘记捕获它时会发生什么?这对我来说很危险。
我认为显示堆栈跟踪的程序是错误的。不喜欢检查异常的人会不会有这种感觉?
所以,如果你不喜欢被检查的异常,你能解释为什么不能,回答那些没有得到回答的问题吗?
编辑:我不想寻求关于何时使用这两种模型的建议,我要寻找的是为什么人们从RuntimeException扩展,因为他们不喜欢从异常扩展和/或为什么他们捕获一个异常,然后重新引发RuntimeException,而不是向他们的方法添加throw。我想了解不喜欢检查过的异常的动机。
- 我不认为这是完全主观的——这是一种语言特性,旨在有一个特定的用途,而不是让每个人自己决定它的用途。它不是特别具有争议性,它提前提出了人们很容易想到的具体的反驳。
- 来吧。作为一种语言特征,这个话题已经并可以客观地探讨。
- @克莱图斯"回答你自己的问题"如果我有答案,我就不会问这个问题了!
- 我同意应该是连续波。没有"正确"的答案,而是关于方法和实践。不过,我不同意应该关闭它。这是一场重要的辩论,关于这个问题的意见越多,我的看法就越好。
- 我不是在问"关于方法论和实践的正确答案",而是在问选择这种特定方法论的人的理由。我没有要求不同处理例外情况的方法之间的相对优势。也不是CW IMO,这是为了比较两者
- 好问题。在C++中,根本没有检查异常,并且在我看来,它使异常特性无法使用。你最终会遇到这样一种情况:你必须在你所做的每一个函数调用周围放置一个catch,因为你只是不知道它是否会抛出一些东西。
- @tofubeer:你是在过度思考这个问题:问你自己以下问题:检查异常在OOA/OOD级别在哪里起作用?如果没有检查异常的概念,95%的语言是完全快乐的?检查异常是人们必须处理的Java特性,因为有损坏的API已经被写在这个破碎的概念周围。这里是200kloc代码库。我们定义了零检查异常,并抛出了零检查异常。我们的OOP离我们的OOA/OOD很近。这就是我们不喜欢他们的原因。
- 如果没有检查异常,C++会被破坏——你经常看到catch(…)只是为了确保事情不会崩溃。因此,既然您有大量的代码库,请回答这个简单的问题——如何确保您不会错过任何异常?假设你添加了一个新的"fooexception"——你如何确保你的程序不崩溃,如果你没有抓住它?你是如何在所有正确的地方捕捉到它的?
- usign tdd和rutime异常通过选中的异常(如果有人认为他可能会错过某些东西)来解决问题。但不幸的是,Java核心LIBS会强迫你捕捉这些异常,这会使代码变得更大,通常没有理由。我认为使用AOP/Spring也可以处理一些问题——Java上的一些问题。
- @SESTdd不能解决这个问题,除非你查看所有的源代码来知道要测试什么。它适用于编写抛出RuntimeException的代码的人,但不适用于编写调用抛出它的代码的人。从程序不能崩溃的前提开始,现在运行时异常有多吸引人?
- @若要提交编写代码的人员,不确定使用此代码的人员(客户机)是否应检查此异常。因为这取决于客户对它的看法。客户机本身可以决定,读取抛出RuntimeException的方法的定义,不管他是否想捕获它。这是公平的。这是对客户时间和克林特密码的尊重。尤其是我没有看到在接口方法中使用checkedExceptions的原因,当1000个类实现它时,其中500个类根本不抛出这个异常,但是客户端仍然必须捕获它。
- @ToSubeer如果代码的客户端使用TDD,它可以帮助他避免坏的情况。
- @SES客户端不一定知道运行时异常,不需要记录运行时异常。如果客户不知道如何做出明智的决定?另外,我的前提是程序永远不会崩溃。运行时异常不适合这样做。
- @ToSubeer也可能不知道如何处理它。这就是一直发生的事情。客户只是吞下它直到有问题为止。因此,客户机应该知道异常/以及他将要使用什么——以及该方法的作用。他一知道这件事就可以决定怎么办。
- 客户将如何准确地了解异常情况?遵循它直到逻辑结论。我给您提供了库X,您从类Z调用方法Y。您应该捕获哪些运行时异常?
- 我个人非常喜欢检查异常,这些异常可以帮助我很早地识别代码中的潜在问题。但我讨厌的一件事是throws声明也是函数签名的一部分。这真的会让你陷入一个完全没有解决方案的问题。这是我认为大多数程序员不喜欢的事情。
- 通过阅读合同?例外情况是方法合同的一部分,不管是否选中。你会发现一些你可以做些事情的例外。
- 这就是@kevin的问题,如果您添加的第三方库没有记录例外情况,那又是什么?检查异常的关键是避免程序员错误…对应该处理的事情(不是程序员错误的事情)使用未经检查的异常会否定这一点。
- @ToSubeer即使您使用的是选中的异常,如果您不记录异常(方法头本身之外)以及它们在什么特定条件下发生,它们也不会知道如何处理您的异常。该问题不是检查异常与未检查异常,而是一个文档。
- 选中的异常鼓励文档化,如果它们没有文档化,那么它们仍然是已知的。未经检查的异常更容易不被记录,更糟糕的是,直到它们在生产环境中发生时才会被知道。
- @ DimitriC。这就是我现在作为C开发人员的感受!
- "虚拟机有花生过敏症,有人把一罐花生掉在它上面"-是的,你肯定希望你的机器在最关键的时刻不可能恢复…我看不出有什么问题。绝不使用错误
- 似乎没人注意到接口的问题。如果实现接口,则只能抛出接口中声明的异常。使得在许多不同的情况下不可能对带有检查异常的错误作出反应。看看我的答案。
- 对于检查异常,我所知道的最有力的论点是,它们在爪哇不是原来存在的,当它们被引入时,它们在JDK中发现了数以百计的错误。这在Java 1之前有点。我个人不会没有他们,在这方面我强烈反对布鲁斯·埃克尔和其他人。
我想我读了和你一样的布鲁斯·埃克尔的采访——这总是困扰着我。事实上,这场争论是被采访者(如果这真的是你所谈论的职位)anders hejlsberg提出的,她是.net和c背后的天才女士。好的。
http://www.artima.com/intv/handcuffs.html
Ok.
虽然我是希斯堡和他的作品的粉丝,但这场争论总是让我觉得是假的。基本上可以归结为:好的。
"Checked exceptions are bad because programmers just abuse them by always catching them and dismissing them which leads to problems being hidden and ignored that would otherwise be presented to the user".
Ok.
我的意思是,如果您使用运行时异常,懒惰的程序员将忽略它(而不是用空的catch块捕获它),并且用户将看到它。好的。
总结的论点是,"程序员不能正确地使用它们,并且不能正确地使用它们比没有它们更糟糕"。好的。
这一论点有一定的道理,事实上,我怀疑小鹅不会在Java中引入操作符重写的动机来自于一个类似的论点——他们混淆了程序员,因为他们经常被滥用。好的。
但最后,我发现这是一个虚假的Hejlsberg的论点,可能是一个事后的论点,用来解释这种缺乏,而不是一个经过深思熟虑的决定。好的。
我会争辩说,虽然过度使用检查异常是一件坏事,而且往往会导致用户处理的草率,但是正确使用这些异常可以让API程序员为API客户机程序员带来巨大的好处。好的。
现在,API程序员必须小心,不要到处抛出检查过的异常,否则它们只会使客户机程序员恼火。正如Hejlsberg警告的那样,非常懒惰的客户机程序员将求助于抓到(Exception) {},所有的好处都将丢失,地狱将随之而来。但在某些情况下,没有什么可以替代检查良好的异常。好的。
对于我来说,经典的例子是文件打开API。语言历史上的每一种编程语言(至少在文件系统上)都有一个API,可以让您在某个地方打开一个文件。而且每个使用这个API的客户机程序员都知道他们必须处理他们试图打开的文件不存在的情况。让我换个说法:每个使用此API的客户机程序员都应该知道他们必须处理这种情况。还有一个问题:API程序员是否可以通过单独评论来帮助他们了解应该如何处理它,或者他们是否确实可以坚持让客户机处理它。好的。
在C语言中,这个成语有点像好的。
1 2
| if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ... |
其中fopen通过返回0表示失败,而c(愚蠢地)允许您将0视为布尔值,并且…基本上,你学会了这个习语,你就没事了。但如果你是个笨蛋,却没有学会这个成语怎么办?那么,当然,你从好的。
1 2
| f = fopen("goodluckfindingthisfile");
f.read(); // BANG! |
学习艰辛的方法。好的。
注意,我们这里只讨论强类型语言:有一个明确的概念,即一个API在强类型语言中是什么:它是一个功能(方法)的无组织文件,您可以用一个明确定义的协议来为每种语言使用。好的。
明确定义的协议通常由方法签名定义。这里fopen要求您给它传递一个字符串(对于c,传递一个char*)。如果你给它其他东西,你会得到一个编译时错误。您没有遵循协议-您没有正确使用API。好的。
在某些(模糊的)语言中,返回类型也是协议的一部分。如果在某些语言中尝试调用等效的fopen(),而不将其赋给变量,则还会出现编译时错误(只能使用void函数)。好的。
我想说的是:在静态类型语言中,如果客户端代码出现明显错误,API程序员会通过阻止客户端代码编译来鼓励客户端正确地使用API。好的。
(在一种动态类型语言中,比如Ruby,您可以传递任何东西,比如一个float,作为文件名——它将编译。如果您甚至不想控制方法参数,为什么还要用选中的异常来麻烦用户呢?此处的参数仅适用于静态类型语言。)好的。
那么,检查异常呢?好的。
这里有一个Java API可以用来打开一个文件。好的。
看到了吗?下面是该API方法的签名:好的。
注意,FileNotFoundException是一个选中的异常。好的。
API程序员对您这样说:"可以使用此构造函数创建新的fileinputstream,但是好的。
a)必须将文件名作为弦b)必须接受文件可能不会在运行时找到"好的。
这就是我所关心的全部问题。好的。
关键是问题的状态基本上是"程序员无法控制的事情"。我的第一个想法是,他/她指的是API程序员无法控制的事物。但事实上,正确使用时检查的异常应该是客户机程序员和API程序员都无法控制的情况。我认为这是避免滥用检查异常的关键。好的。
我认为打开的文件很好地说明了这一点。API程序员知道你可能会给他们一个在调用API时并不存在的文件名,他们将无法返回你想要的,但必须抛出一个异常。他们还知道这种情况会经常发生,客户机程序员可能会期望在编写调用时文件名是正确的,但在运行时也可能是错误的,原因超出了他们的控制范围。好的。
因此,API明确表示:在你给我打电话的时候,有些情况下,这个文件不存在,你最好处理好它。好的。
这一点在反诉中会更清楚。假设我正在编写一个表API。我在某个地方有一个包含以下方法的API表模型:好的。
1
| public RowData getRowData(int row) |
现在作为一个API程序员,我知道有些情况下,有些客户机会传递行的负值或表外的行值。因此,我可能会抛出一个选中的异常并强制客户机处理它:好的。
1
| public RowData getRowData(int row) throws CheckedInvalidRowNumberException |
(我当然不会称之为"检查过的")。好的。
这是对选中异常的错误使用。客户机代码将充满获取行数据的调用,其中每一个都必须使用try/catch,为什么呢?他们是否要向用户报告查找了错误的行?可能不是-因为无论我的表视图周围的UI是什么,它都不应该让用户进入请求非法行的状态。所以这是客户端程序员的一个错误。好的。
API程序员仍然可以预测客户机将对这些错误进行编码,并应使用运行时异常(如IllegalArgumentException)对其进行处理。好的。
在getRowData中有一个选中的异常,很明显这是一个将导致Hejlsberg懒惰的程序员简单地添加空捕获的情况。当这种情况发生时,即使测试人员或客户机开发人员调试也不会发现非法的行值,相反,它们将导致难以确定其来源的连锁错误。阿里安娜火箭将在发射后爆炸。好的。
好吧,问题是:我是说,检查异常FileNotFoundException不仅是一件好事,而且是API程序员工具箱中的一个基本工具,用于以对客户端程序员最有用的方式定义API。但是CheckedInvalidRowNumberException是一个很大的不便,导致了糟糕的编程,应该避免。但如何区分。好的。
我想这不是一个确切的科学,我想这是基础,也许在一定程度上证明了赫杰斯伯格的论点。但是我不喜欢把孩子和洗澡水一起扔出去,所以请允许我在这里提取一些规则来区分好的检查异常和坏的检查异常:好的。
超出客户控制或关闭与打开:好的。
只有当错误情况超出API和客户机程序员的控制范围时,才应使用选中的异常。这与系统的打开或关闭程度有关。在一个受约束的UI中,客户机程序员可以控制所有按钮、键盘命令等,这些按钮、键盘命令等可以从表视图(一个封闭的系统)中添加和删除行,如果它试图从一个不存在的行中获取数据,那么这就是客户机编程错误。在一个基于文件的操作系统中,任何数量的用户/应用程序都可以添加和删除文件(一个开放系统),可以想象客户机请求的文件已经被删除,而他们不知道,因此他们应该处理它。好的。
无所不在:好的。
选中的异常不应用于客户端频繁进行的API调用。我经常指的是客户机代码中的许多地方——不经常是在时间上。因此,客户机代码不会经常尝试打开同一个文件,但是我的表视图使用不同的方法在整个地方获取RowData。尤其是,我要写很多类似的代码好的。
1
| if (model.getRowData().getCell(0).isEmpty()) |
每次都要尝试/捕获,这会很痛苦。好的。
通知用户:好的。
在可以想象向最终用户显示有用的错误消息的情况下,应该使用选中的异常。这就是"当它发生时你会怎么做?"我在上面提出的问题。还涉及第1项。由于您可以预测客户机API系统之外的某些内容可能会导致文件不在其中,因此您可以合理地告诉用户:好的。
1
| "Error: could not find the file 'goodluckfindingthisfile'" |
由于您的非法行数是由内部错误引起的,而且用户没有任何错误,所以您实际上无法提供有用的信息。如果你的应用程序不允许运行时异常进入控制台,它可能会给它们一些难看的信息,比如:好的。
1
| "Internal error occured: IllegalArgumentException in ...." |
简而言之,如果您认为您的客户机程序员不能以一种帮助用户的方式解释您的异常,那么您可能不应该使用选中的异常。好的。
所以这是我的规则。有点做作,肯定会有例外(如果你愿意,请帮助我完善它们)。但我的主要论点是,在一些情况下,比如FileNotFoundException,选中的异常与参数类型一样重要,也是API契约中有用的一部分。所以我们不应该仅仅因为它被滥用就放弃它。好的。
抱歉,我不是故意这么长时间胡扯的。最后,我提出两个建议:好的。
A:API程序员:谨慎地使用检查过的异常以保持它们的有用性。如果有疑问,请使用未经检查的异常。好的。
B:客户机程序员:在开发早期养成创建一个打包的异常(google it)的习惯。JDK1.4及更高版本在RuntimeException中为此提供了一个构造函数,但您也可以轻松地创建自己的构造函数。建造师如下:好的。
然后养成这样的习惯:每当你必须处理一个检查过的异常,而你觉得自己很懒惰(或者你认为API程序员在一开始就过分热衷于使用检查过的异常),不要仅仅吞下这个异常,包装它,然后重新处理它。好的。
1 2 3 4 5 6
| try {
overzealousAPI (thisArgumentWontWork );
}
catch (OverzealousCheckedException exception ) {
throw new RuntimeException(exception );
} |
把它放在你的一个IDE的小代码模板中,当你觉得懒惰的时候使用它。这样,如果您真的需要处理检查过的异常,您将不得不在运行时看到问题后返回并处理它。因为,相信我(还有安德斯·海斯伯格),你永远不会在你的好的。
1
| catch (Exception e ) { /* TODO deal with this at some point (yeah right) */} |
好啊。
- 打开文件实际上是一个很好的例子,因为完全起反作用的检查异常是。因为大多数时候打开文件的代码不能对异常做任何有用的事情——最好在调用堆栈上做几个层。选中的异常迫使您混乱方法签名,这样您就可以做您本来应该做的事情——在最有意义的地方处理异常。
- @迈克尔:同意你可能会处理好这些IO异常,但我听不到你否认作为一个API客户,你必须预料到这些。因此,我认为检查异常是适当的。是的,您必须在调用堆栈上的每个方法上声明"throw",但我不同意这是一种混乱。它是方法签名中有用的声明性信息。您的意思是:我的低级方法可能会遇到一个丢失的文件,但它们会将处理权留给您。我看不到任何损失,只有好的、干净的代码才能获得
- @大黄:+1,非常有趣的答案,显示了双方的论点。伟大的。关于上一条注释:请注意,在调用堆栈上声明对每个方法的抛出可能并不总是可能的,特别是在实现接口的过程中。
- 这是一个非常有力的论据(一般来说我也同意),但有一种情况是它不起作用的:通用回调。如果我有一些库调用了一些用户定义的代码,那么我就不可能知道Java从调用它的用户代码的用户代码中传播检查异常。这个问题还扩展到许多静态类型语言的类型系统的其他部分,这些语言不支持适当的泛型类型。如果提到这个(也许还有一个回应),这个答案将会得到改进。
- 感谢您的详细答复。但是,我确实同意其中的大部分,我不认为"普遍性"或"通知用户"是有效的因素。第一点是,当错误超出了API程序员的控制和API客户端的控制时,验证检查异常的使用所需的全部内容。我遇到的检查异常最明显的误用是org.json.jsonObject的API。在处理预先验证的JSON内容时,正确的做法是编写"try,da da da da da,catch,curly curly",否则我将在catch子句中编写死代码。
- @Jameswald:我假设,对于一个方法永远不应该抛出的检查异常,正确的行为是将它们包装在某种运行时错误中;实际上,我进一步假设99%对检查异常的抱怨源于缺乏一种简洁的方式来声明性地表达它,而不是强制性地表达它。
- @Supercat自从我上次写这篇文章以来,我已经学会了使用运行时异常(或者适当的断言错误)进行包装和抛出。
- @jameswald:你同意如果有一种方法在声明"doesntachefooexception,barexception"中包装一个块,并让编译器自动执行适当的catch wrap rethrow,这会是件好事吗?
- 我认为Java 7的多捕获异常语法是一个很好的折衷方案。它看起来非常像现有的Try-Catch块,它不需要新的字节码,所以它也是向后兼容的。我现在使用它来Android开发,即使在Android 4.4 KITKAT之前的设备不支持Java 7的新字节码来尝试资源。
- @jameswald:doesntcatch指令不需要新的字节码,我认为它比捕获和重新发送、捕获和吞咽,或者更糟的是,throws指令更好地传达了程序员的意图。如果一个方法知道检查异常的含义,并且知道程序流的中断不会导致其他东西被破坏,那么它应该只允许检查异常渗出来。如果一个方法调用另一个方法,而该方法不希望实际抛出已检查的异常,但无论如何都会这样做,则渗透出该异常可能会导致调用方错误地认为它可以处理导致该异常的条件。
- 错误@大黄。选中的异常用于"意外事件",可以处理,但始终与"早抛、晚抓"最佳实践不兼容。打开文件是一个精心挑选的示例,可以对其进行处理。大多数IOException(例如写入字节)、SQLException、RemoteException都无法以有意义的方式处理。失败或重试应该处于"业务"或"请求"级别,并且检查异常(如Java库中使用的)是一个错误,使之变得困难。识字java.com/exceptions/…
- 讽刺的是,你唯一的专家意见——布鲁斯·埃克尔/安德斯的采访——不同意你的立场。在采访中,布鲁斯和安德斯都同意检查例外在爪哇是一个严重的错误,安德斯感觉很好,他躲避在C,布鲁斯在JAVA感到遗憾。
- 我认为这整个答案很少关注OP的案件问题与检查异常。
- 我认为这个答案从根本上忽略了一点,即抛出异常的地方很少靠近您想要处理它的地方。因此,如果您的层次结构中有很深的数据库调用抛出SQLException,那么您将不得不使用throws SQLException污染Web应用程序中的几乎所有函数,这样全局处理程序就可以捕获它并返回500。这种无意识的重复毫无益处。throws声明变成了毫无意义的噪音,你必须训练自己去忽视它,如果你这样做,你就得不到应有的好处。
- @大黄,我不明白你为什么认为我检查没有找到的文件很重要,但我也检查了"DriveBusy"、"Outofhandles"、"UnfectPermission"和无数其他特定的错误案例,这些都意味着"你不能打开该文件"。
- @JPMC26您不必使用sqlException抛出强制污染任何内容,这样做意味着您没有正确处理检查过的异常。throws子句是接口的一部分,将实现细节放入其中是不好的设计。无论获取sqlExceptions的代码是什么,如果它不能有效地处理它们,则应该捕获它们并抛出一些其他异常,这些异常表示该错误在该函数域中的含义(可能是在记录该异常的详细信息以便调试之后),或者如果更合适,则抛出未检查的异常。
- @Erics在Web请求期间处理意外的SQLException的正确方法是让它冒泡到全局处理程序并导致500个错误;不能采取其他有用的操作。这样的全局处理程序通常是调用代码的Web框架的一部分或注册到Web框架中。即使你是正确的,一堆try/catch/throw集比throws声明更具锅炉板/污染/再现性,而且隐藏实际错误的额外缺点更为严重,这使得更难理解错误的原因。
- @JPMC26保持接口和实现之间的分离是非常重要的。如果出现任何混乱,则应将其包含在未泄漏到接口(即throw)的实现代码中。您的Web框架应该能够运行异常,如果不适合在代码中处理,您可能希望将sqlException包装在其中。
- @在未检查的异常中包装某些东西对于将实现与接口分离没有任何作用。调用方仍然必须准备好抛出异常。您编写的每一行代码都应该在这样的假设下编写:它调用的某个代码有一天会抛出一个您没有预料到的异常,因此在这种情况发生时,它应该尽最大努力不让任何代码处于错误的状态。无论函数声明上是否有EDOCX1[2],都不能解决实际问题,并且您所提倡的做法会降低开发人员的生产效率,而不会带来任何好处。
- @JPMC26在我看来,您似乎错过了未检查异常的要点。您的函数已经可以潜在地抛出未检查的异常,因此,在某种程度上,某些东西应该处理它,并且应该已经处理它,因此包装一个sqlException,使您的函数不能在runtimeException中做任何有用的事情,这不会增加额外的开销。但是,使函数抛出sqlException会产生很大的影响并污染接口。您也可能无法控制正在实现的接口,使其引发sqlException甚至可能不是一个选项。
- @艾丽斯,我没有漏掉这一点。我是说,检查过的异常没有价值,它们周围的所有东西都是无用的样板,实际上并不能提高生产力或质量,甚至是稍微提高一点。它实际上并没有解决任何问题,包括它承诺要解决的问题:强迫开发人员处理它们。事实上,你建议用未检查的异常包起来避免检查过的异常,这只会加强我的论点。
- 这不是我的建议。检查异常是有用的,它们迫使程序员决定如何使用抛出异常的函数在代码中处理异常(但这与我们一直争论的不同)。决定代码不能有效地处理它们,并将它们"翻译"为未检查的异常,是可以选择的几种可能性之一。
- 让我们在聊天中继续讨论。
- 这个答案的主要问题是,它完全歪曲了Hejlsberg反对检查过的异常的实际论点——事实上,它鼓励坏的做法只是其中一个论点,如果你问我的话,并不是最令人信服的。更准确的tl;dr实际上是接口污染方面,特别是在未来可能引入新类型的检查异常的变化中——那么您真的处于一个绑定中,因为这对所有调用方都是一个破坏性的变化。正是这个根本问题导致了坏的做法
关于检查异常的事情是,根据对概念的通常理解,它们并不是真正的异常。相反,它们是API可选返回值。
异常的整体概念是,在调用链的某个地方抛出的错误可能冒泡,并由更高的某个地方的代码处理,而不必担心干预代码。另一方面,检查异常要求抛投者和接球者之间的每一级代码声明他们知道可以通过它们的所有异常形式。实际上,这与if-checked异常只是调用方必须检查的特殊返回值几乎没有什么不同。例如[伪代码]:
由于Java不能执行替代的返回值,或者简单的内联元组作为返回值,检查异常是合理的响应。
问题是,许多代码,包括大量的标准库,在实际的异常情况下误用检查异常,您可能非常希望赶上几个级别。为什么IOException不是RuntimeException?在其他每种语言中,我都可以让IO异常发生,如果我不做任何处理,应用程序将停止,我将得到一个方便的堆栈跟踪来查看。这是可能发生的最好的事情。
可能有两种方法,从整个写到流过程中捕获所有的异常,中止进程并跳进错误报告代码;在Java中,不需要在每个调用级别添加"抛出IOExtExchange",即使它们本身不执行IO级别也不能这样做。这种方法不需要吗?了解异常处理;必须在其签名中添加异常:
不必要地增加耦合;
使接口签名很容易更改;
使代码的可读性降低;
非常烦人,程序员通常的反应是做一些可怕的事情来击败系统,比如"抛出异常"、"捕获(异常E)",或者将所有东西包装在RuntimeException中(这使得调试更加困难)。
还有很多可笑的图书馆例外,比如:
1 2 3 4 5
| try {
httpconn. setRequestMethod("POST");
}?catch (ProtocolException e ) {
throw new CanNeverHappenException ("oh dear!");
} |
当你不得不用这样可笑的代码混乱你的代码时,难怪被检查的异常会收到一堆讨厌的东西,即使这真的只是简单糟糕的API设计。
另一个特别的不良影响是控制反转,其中组件A向通用组件B提供回调。组件A希望能够让异常从其回调中抛出到它调用组件B的位置,但它不能这样做,因为这会更改由B修复的回调接口。A只能通过wrappi来完成。在runtimeexception中生成真正的异常,这是一个需要编写的异常处理样板。
在Java及其标准库中实现的检查异常意味着样板、样板、样板。用一种已经冗长的语言来说,这不是一个胜利。
- 在你的代码的例子,这将是最好的链的异常引起的,可以发现在原来的日志:掷canneverhappenexception里丁();
- 我disagree。异常,或不检查,在特殊的条件。例如:这是一retrieves HTTP对象的方法。返回的值是一个对象或其他什么,所有的事情都可以去坏的例外。返回值的处理,他们只做它在C到普尔线索混乱的设计。
- "我"米先生说:那是在Java执行检查的异常表现在实践中,更多的返回值在C类的比他们的传统的"例外","我们可能承认从C + +,Java语言和其他前。事实上,这是国际海事组织(IMO)和导致混乱和不差的设计。
- 同意标准库误用选中的异常肯定会增加混乱和不良的捕获行为。而且,通常它只是来自糟糕的文档,例如,当"发生其他I/O错误"时,像disconnect()这样的分解方法会抛出IOException。好吧,我断开了!我是在泄漏句柄还是其他资源?我需要重试吗?不知道为什么会发生这种情况,我无法得出我应该采取的行动,因此我不得不猜测我应该吞下它,再试一次,还是保释。
- +1表示"API备选返回值"。查看已检查异常的有趣方式。
- 我喜欢"另类回报价值"的概念。有些异常类型可能对除直接调用方之外的任何人都不有用,即使调用方无法处理异常,调用方也应以这样的方式包装它,使其调用方知道它是从嵌套例程中抛出的)。我认为检查异常的大问题与异常的大问题联系在一起:太多不同的事情都被异常的类型所包围。Java基于异常类型区分"检查与未检查"异常的事实使情况变得更糟,但是…
- …不是根本问题。可能有用的是,对于一个"catch"语句来说,有一个可靠的方法来表示它只想捕获立即调用的例程希望"获得信任"的异常,对于例程来说,有一个方便的方法来表示它们希望何时对嵌套的例程(甚至它们自己的,fo)抛出的异常获得信任。r那很重要)。如果有一种方便的方法声明性地指示应该包装异常,而不必编写命令式代码,那么选中的异常可以提供某种程度的支持。
- 但是,在许多方面,需要的是标准的"聚合异常"类型,具有不同的"catch"和"resolve"语句;如果"catch"语句的任何条件与聚合异常匹配,"catch"语句应运行代码,但聚合异常应在调用堆栈上继续运行,直到所有部分都"解析"。在Java或.NET中没有这样的特性,但我认为这样的特性应该是开发的新框架的一部分。
- @在编程中,"异常条件"并不意味着我们可以忽略的条件。这意味着我们知道,尽管我们尽了最大努力,最终还是会有一天发生,但我们没有一个好的方法来解决这些问题。对于在保存文件的过程中拔掉磁盘插头的情况,OpenOffice无法合理地执行任何操作。如果您的代码有一个bug并且传入了一个非法的null,API就帮不了它。对处理我们的代码无法有效处理的情况施加严格的要求是没有意义的,"最佳方法"将非常依赖上下文。
- 我认为从概念上讲,将异常作为可选返回值的想法是有意义的,但我会更进一步。这是另一种回报机制。异常可以通过函数调用堆栈中的多个条目传递相同的值,从而在进程中无提示地绕过大量代码。这不是正常的return机制所能做的,这也是异常允许我们实现解耦的原因。归根结底,例外是流程控制,这与陈词滥调相反。它们是一个更有限、更易于管理的goto(因为对状态有更大的保证)。
- 与错误返回值不同,每次函数调用后都不必检查异常。try-catch块可以对函数进行分组,这样错误处理就不会与算法交错。因此,将检查的异常与返回值相等是不正确的。
我将只选择一个原因,而不是针对已检查的异常重新分析所有(许多)原因。我记不清我写这段代码的次数:
1 2 3 4 5
| try {
// do stuff
} catch (AnnoyingcheckedException e ) {
throw new RuntimeException(e );
} |
99%的时候我什么都做不到。最后,块进行任何必要的清理(或者至少应该进行清理)。
我也不知道我见过多少次了:
1 2 3 4 5
| try {
// do stuff
} catch (AnnoyingCheckedException e) {
// do nothing
} |
为什么?因为有人不得不处理它,而且很懒惰。这是错的吗?当然。发生了吗?当然。如果这是未检查的异常呢?这个应用程序可能已经死了(这比吞下一个例外要好)。
然后我们有一些恼人的代码,将异常作为流控制的一种形式,就像java.text.format那样。Bzzzt。错了。用户在表单的数字字段中输入"abc"也不例外。
好吧,我想这是三个原因。
- 但是,如果一个异常被正确捕获,您可以通知用户,执行其他任务(日志?)并以受控方式退出应用程序。我同意一些API部件可以设计得更好。出于懒惰的程序员的原因,我认为作为一个程序员,你对你的代码负有100%的责任。
- 请注意,try-catch-rethrow允许您指定一条消息—我通常使用它来添加有关状态变量内容的信息。IOExceptions的一个常见示例是添加相关文件的absolutePathName()。
- 我认为你发现了最大的问题,除了检查异常,如果foo打电话给bar,bar可能会意外地抛出wazooExceptionfoo不准备处理,那么foo应该有一个简单的方法简单地声明它不准备处理wazooException来自bar的wazooException。通过throws传递是错误的方法,因为调用方无法区分foo抛出的wazooException,原因是foo预期,而bar抛出的wazooException,原因是foo未预期。
- 我喜欢你的第三个原因(这是反对检查异常的原因吗?不管怎样,我喜欢它)。但是由于懒惰的程序员的原因,我肯定会把这样一个程序员从我的团队中踢出去。
- 我认为像Eclipse这样的IDES对于您看到空catch块的次数负有很大的责任。实际上,它们应该在默认情况下重新发送。
- "99%的时候我什么都做不到"--错了,你可以给用户显示一条消息,说"无法连接到服务器"或"IO设备失败",而不是让应用程序因为网络小故障而崩溃。你的两个例子都是来自坏程序员的工作艺术。您应该攻击坏的程序员,而不是检查异常本身。就像我在用胰岛素做沙拉酱时攻击它,因为它对我的糖尿病没有帮助。
- @你不能总是做这些事情。有时候,你有一个界面需要遵循。当你设计出好的可维护的API时,这尤其正确,因为你不能开始到处乱扔副作用。这两者,以及它所带来的复杂性,都会在很大程度上严重地伤害你。所以是的,99%的时候,没有什么可以做的。
- @mastermastic为什么实现接口的简单行为会阻止您在网络连接丢失或文件加载失败时向用户发出警报?
- 如果我错了,请原谅并纠正我(我对Java不太熟悉),但是如果接口不期望异常,那么,作为实现者,我该怎么办呢?&如何提醒用户?我是否必须将错误日志界面带到可能失败的每个对象上?&那么呢?我如何退出程序?或者我应该像别人说的那样退出?这些都是可怕的做法,不是吗?模块化是死的,复杂性是可悲的,如果我刚退出应用程序,我获得的所有资源(不由操作系统管理)会发生什么?.
- @mastermastic如果一个接口没有声明一个方法来抛出一个选中的异常,那么它的实现也不能。在接口的特定实现遇到检查过的异常的罕见情况下,该特定实现的责任就是处理它,毕竟,接口的调用代码并不期望出现这种异常,用一个异常来打它们是没有意义的。理想情况下,您会返回一个适当的结果,或者您总是可以将检查的异常包装在一个未检查的异常中,并抛出它。
- @Yasmanillanes我还没有发现这是罕见的。我认为接口不应该预测所有的异常。如果您将获得任意字符串的服务接口起来,实现者可以通过从静态内存中读取或从本地文件、网络或远程数据库中获取来实现,并且存在无限的可能性。我确信您不认为接口应该预测所有可能的(无限的)异常。所以,我不认为对例外情况采取措施是不合理的。当然,这离理想还远着呢,但是,呃,这并不是新闻,例外是一种可怕的机制。
- 是的,最后我真的抛出了一个runtimeexception来绕过检查过的异常(我认为这很糟糕)。你说的是"适当的结果",但根据我的经验,这通常是不可能的(至少对我们这些认为无效决不是适当的)。非常感谢您的回复。
- @mastermastic它取决于接口声明的内容。如果接口正在声明类似于"loadstagefile()"的东西,那么IOException是给定的;接口应该声明它。如果接口声明类似于"calculatePidigit()"的东西,那么在实现中可能会发生任何异常。如果您的实现出于任何原因从一个文件计算PI,那么捕获IOExceptions是您的工作,在这种情况下,您可能希望将它包装在RuntimeException中。
- @mastermastic记住,这些带有接口的案例甚至不是一个展示的障碍。选中的异常不会从特性方面带走任何东西。如果你想要的话,你总是可以自由使用RunTimeExtExcor,但是这个IOExchange总是应该被处理的共同智慧仍然存在,Java只是确保你坚持这个共同的智慧。
- @公理化发出"信息……而不是让应用程序崩溃"。这始终是一个选项,但这通常发生在10-20个堆栈帧更高的情况下,这些帧都会被选中的异常所污染。通常情况下,只在靠近顶部框架的某个地方编写catch Exception会更好,因为这样你就可以确定什么都不会错过。显然,您可以并且通常应该根据确切的异常类来区分,但这不是您需要检查异常的原因。事实上,它们会使事情变得更糟,因为你经常被迫包装它们,然后检查原因。
- @马阿蒂努斯我几乎不会称之为污染加上两个词:"抛出Xyzexception"到堆栈上任何方法的签名,你知道的……引发XyzeException。好处(确保正确处理异常)远远大于成本(在方法签名中添加两个字)。
- @你认为java.text.format应该怎么做而不是抛出异常?
- @莫汉:Format的例子很搞笑,就在抛出方法的正上方,有一个方法parseObject(String source, ParsePosition pos)不抛出,但在错误输入时返回null并相应地更新位置对象。程序员有选择权。
好吧,这不是关于显示stacktrace或无声崩溃。它是关于能够在层之间传递错误。
检查异常的问题是,它们鼓励人们接受重要的细节(即异常类)。如果您选择不吞咽这个细节,那么您必须在整个应用程序中不断添加throw声明。这意味着1)新的异常类型将影响许多函数签名,2)您可能会错过实际要捕获的异常的特定实例(例如,为将数据写入文件的函数打开辅助文件)。辅助文件是可选的,因此您可以忽略它的错误,但是由于签名throws IOException,很容易忽略这一点)。
我现在正在一个应用程序中处理这种情况。我们将几乎所有异常重新打包为AppSpecificException。这使得签名变得非常干净,我们不必担心签名中的throws会爆炸。
当然,现在我们需要在更高级别专门化错误处理,实现重试逻辑等等。但是,一切都是AppSpecificException,因此我们不能说"如果抛出IOException,请重试"或"如果抛出ClassNotFound,请完全中止"。我们没有一种可靠的方法来处理真正的异常,因为当事情在代码和第三方代码之间传递时,会一次又一次地重新打包。
这就是为什么我非常喜欢Python中的异常处理。你只能抓住你想要和/或能处理的东西。其他的一切都会冒泡起来,就好像你自己把它重新穿上一样(不管你做了什么)。
我发现,在我提到的整个项目中,异常处理分为三类:
捕获并处理特定的异常。例如,这是为了实现重试逻辑。
捕获并重新引发其他异常。这里所发生的一切通常是日志记录,它通常是一条陈腐的消息,比如"无法打开$filename"。这些都是你做不到的错误;只有更高层次的人知道足够的能力来处理它。
捕获所有内容并显示错误消息。这通常位于调度器的根目录,它所做的一切就是确保它能够通过非异常机制(弹出对话框、封送RPC错误对象等)将错误传递给调用者。
- 您可以创建AppSpecificException的特定子类,以便在保留纯方法签名的同时允许分离。
- 对第2项的另一个非常重要的补充是,它允许您向捕获的异常添加信息(例如,通过嵌套在RuntimeException中)。在堆栈跟踪中找不到该文件的名称要比隐藏在日志文件的深处要好得多。
- 基本上,你的论点是"管理异常很累,所以我宁愿不处理它"。当异常冒泡时,它会失去意义,上下文创建实际上是无用的。作为一个API的设计者,如果我的程序因为没有被告知这个或那个异常会"冒泡"而崩溃,那么作为一个设计者,作为失败的结果,我的系统就没有它所能达到的那样稳定,那么你应该在合同上明确地说明当事情出了问题时会发生什么。
- 这根本不是我说的。你的最后一句话实际上与我相符。如果所有的东西都被包裹在AppSpecificException中,那么它就不会冒泡(含义和上下文丢失),而且,是的,API客户端没有被通知——这正是被检查异常(如Java中)所发生的情况,因为人们不想处理具有大量EDCOX1×4声明的函数。
- @newtopian——异常在很大程度上只能在"业务"或"请求"级别处理。大粒度的失败或重试是有意义的,而不是每一个微小的潜在失败。因此,异常处理最佳实践总结为"提前抛出、延迟捕获"。检查异常使得在正确的级别上管理可靠性变得更加困难,并鼓励大量错误编码的catch块。识字java.com/exceptions/…
- 你可以在你的AppSpecificException中添加一个类似hasCause(Class extends Exception>... causeClasses)的方法(搜索整个链),并像使用多个catch子句一样容易地使用它。
- @maaartinus或只对AppSpecificException使用工厂方法,该方法不创建新实例,但返回相同类型的原因(如果存在)。
- 您的python示例在功能上相当于未选中的异常(如果您不希望处理它们,它们会冒泡到可以很好地处理或至少显示它们的位置),我完全同意——强制在调用点处理异常并不可取,这是对问题的一个很好的回答。
我知道这是一个古老的问题,但我花了一段时间与检查过的异常进行斗争,我还有一些补充。请原谅我这么久!好的。
我的主要观点是他们破坏了多态性。不可能让它们很好地处理多态接口。好的。
采用好的OL’Java EDCOX1×0接口。我们有常见的内存实现,如ArrayList和LinkedList。我们还有骨骼类AbstractList,这使得设计新类型的列表很容易。对于只读列表,我们只需要实现两种方法:size()和get(int index)。好的。
此示例WidgetList类从文件中读取Widget类型(未显示)的某些固定大小对象:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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接口公开小部件,您可以检索项目(list.get(123)或迭代列表(for (Widget w : list) ...而无需了解WidgetList本身。可以将此列表传递给任何使用通用列表的标准方法,或者将其包装在Collections.synchronizedList中。使用它的代码既不需要知道也不需要关心"小部件"是由现场组成、来自数组、从文件或数据库中读取,还是从网络中读取,或者从未来的子空间中继读取。因为正确实现了List接口,所以它仍然可以正常工作。好的。
除此之外,上面的类不会编译,因为文件访问方法可能会抛出一个IOException,这是一个检查过的异常,您必须"捕获或指定"。您不能将其指定为抛出——编译器不会允许您这样做,因为这样会违反List接口的约定。而且,WidgetList本身无法处理这个异常(我将在后面解释)。好的。
显然,唯一要做的就是捕获并重新将选中的异常作为某些未选中的异常来显示:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @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为这个情况添加了一个EDCOX1×17的类:用于捕获和重新抛出多态性方法边界中的EDCOX1、14个s。有点证明我的观点!)好的。
所以检查异常在这种情况下根本不起作用。你不能扔它们。同样,对于由数据库支持的聪明的Map,或者通过COM端口连接到量子熵源的java.util.Random的实现。一旦您尝试对多态接口的实现做任何新颖的事情,检查异常的概念就失败了。但是检查过的异常是如此的阴险,以至于它们仍然不能让您安心,因为您仍然需要从较低级别的方法中捕获并重新传递任何异常,混乱代码和混乱堆栈跟踪。好的。
我发现普遍存在的Runnable接口如果调用了抛出检查异常的东西,通常会返回到这个角。它不能按原样抛出异常,所以它所能做的就是通过捕获和重新执行RuntimeException来混乱代码。好的。
实际上,如果你诉诸于黑客,你可以抛出未声明的检查异常。在运行时,JVM不关心检查的异常规则,因此我们只需要愚弄编译器。最简单的方法就是滥用仿制药。这是我的方法(类名是因为(在Java 8之前)在泛型方法的调用语法中需要的)显示的:好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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"示例:好的。
1 2 3 4 5 6 7 8
| @Override
public int size () {
try {
return (int)(file. length() / SIZE_OF_WIDGET );
} catch (IOException e ) {
throw sneakyThrow (e );
}
} |
不幸的是,检查异常的最后一个侮辱是编译器拒绝允许您捕获检查异常,如果在它有缺陷的观点中,它不能被抛出。(未选中的异常没有此规则。)要捕获偷偷抛出的异常,必须执行以下操作:好的。
1 2 3 4 5 6 7 8 9 10 11
| 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中的已检查异常的代码简单一些。好的。
令人高兴的是,尽管在Java 7中添加了一个关于重新捕获异常的规则,但是EDCOX1 25的声明在这里是合法的,尽管EDCOX1 OR 26的类型被检查。好的。
当选中的异常满足多态性时,相反的情况也是一个问题:当一个方法被指定为可能会抛出选中的异常,但被重写的实现却没有。例如,抽象类OutputStream的write方法都指定throws IOException方法。ByteArrayOutputStream是一个子类,它写入内存中的数组而不是真正的I/O源。它重写的write方法不能引起IOExceptions,因此它们没有throws子句,您可以调用它们而不必担心捕获或指定要求。好的。
但并非总是如此。假设Widget有一种方法将其保存到流中:好的。
声明这个方法接受一个普通的OutputStream是正确的做法,因此它可以多态地用于各种输出:文件、数据库、网络等等。以及内存阵列。但是,对于内存中的数组,有一个虚假的要求来处理实际无法发生的异常:好的。
像往常一样,检查过的异常会阻碍。如果将变量声明为具有更多开放式异常需求的基类型,则必须为这些异常添加处理程序,即使您知道这些异常不会出现在应用程序中。好的。
但是等等,检查过的异常实际上很烦人,他们甚至不会让你做相反的事情!假设您当前捕获由write调用OutputStream所抛出的任何IOException,但您希望将变量的声明类型更改为ByteArrayOutputStream,编译器将责怪您试图捕获它所说的不能抛出的已检查异常。好的。
这条规则引起了一些荒谬的问题。例如,OutputStream的三个write方法之一不被ByteArrayOutputStream覆盖。具体来说,write(byte[] data)是一种方便的方法,它通过调用write(byte[] data, int offset, int length)来编写完整的数组,偏移量为0,数组长度为。ByteArrayOutputStream重写了三参数方法,但继承了一参数方便方法。继承的方法确实做了正确的事情,但它包含了一个不需要的throws子句。这可能是ByteArrayOutputStream设计中的一个疏忽,但他们永远无法修复它,因为它会破坏与任何捕获异常的代码的源代码兼容性——从未、从未、也永远不会抛出异常!好的。
这条规则在编辑和调试期间也很烦人。例如,有时我会临时注释一个方法调用,如果它可能引发了一个检查过的异常,编译器现在会抱怨本地try和catch块的存在。所以我也要把它们注释掉,现在在编辑代码时,IDE会缩进到错误的级别,因为{和}被注释掉了。啊!这只是一个小小的抱怨,但似乎唯一被检查过的例外情况就是引起麻烦。好的。
我快做完了。我最后对检查的异常感到失望的是,在大多数呼叫站点,您对它们没有任何帮助。理想情况下,当出现问题时,我们会有一个特定于应用程序的处理程序,它可以通知用户问题和/或根据需要结束或重试操作。只有栈中高层的处理程序才能做到这一点,因为它是唯一知道总体目标的处理程序。好的。
相反,我们得到了下面的成语,它作为关闭编译器的一种方式非常猖獗:好的。
1 2 3 4 5
| try {
...
} catch (SomeStupidExceptionOmgWhoCares e) {
e.printStackTrace();
} |
在图形用户界面或自动程序中,不会看到打印的消息。更糟糕的是,它在异常之后继续处理其余的代码。异常实际上不是错误吗?那就不要打印了。否则,其他东西将在一瞬间爆炸,到那时原始异常对象将消失。这个习语不比basic的On Error Resume Next或php的error_reporting(0);好。好的。
调用某种类型的logger类并不好:好的。
1 2 3 4 5
| try {
...
} catch (SomethingWeird e) {
logger.log(e);
} |
这和e.printStackTrace();一样懒惰,仍然在不确定的状态下处理代码。另外,特定日志记录系统或其他处理程序的选择是特定于应用程序的,因此这会影响代码的重用。好的。
但是等等!找到特定于应用程序的处理程序有一种简单而通用的方法。它位于调用堆栈的上方(或者设置为线程的未捕获异常处理程序)。所以在大多数情况下,您需要做的就是将异常抛到堆栈的更高位置。例如,throw e;。选中的异常会妨碍您的工作。好的。
我确信在设计语言时,检查异常听起来是个好主意,但实际上我发现它们都很麻烦,没有任何好处。好的。好啊。
- 对于widgetlist的size方法,我将把大小缓存在一个变量中,并在构造函数中设置它。构造函数可以随意引发异常。但是,如果文件在使用小部件列表时发生更改,这将不起作用,如果发生更改,可能会很糟糕。
- 一个愚蠢的人,除了关心别人的人,还有一个足够关心扔它的人。所以要么它不应该被抛出(糟糕的设计),要么你真的应该处理它。同样的情况也发生在1.0之前的类(字节数组输出流)的错误实现中,不幸的是,设计很糟糕。
- 正确的习惯用法应该是一个指令,它可以捕获嵌套子例程调用所引发的任何指定异常,并用RuntimeException包装它们。请注意,一个例程可以同时声明为throws IOException,但也指定从嵌套调用中抛出的任何IOException都应被视为意外并被包装。
- 我是一个专业的C开发人员,有一些Java经验,在这篇文章中偶然发现。我不明白为什么会有人支持这种奇怪的行为。在.NET中,如果我想捕获特定类型的异常,我可以捕获它。如果我只想让它被扔进垃圾堆,那就没什么可做的了。我希望Java不那么古怪。:)
- 很好的例子。伟大的阅读。完全同意。
- 关于"有时我会临时注释一个方法调用",我学会了使用if (false)。它避免了throw子句的问题,并且警告可以帮助我更快地返回。+++也就是说,我同意你所写的一切。检查异常具有一定的价值,但与成本相比,该价值可以忽略不计。他们几乎总是挡道。
- 您的instanceof片段可以简化。
信噪比
首先,检查异常会降低代码的"信噪比"。AndersHejlsberg还谈到了命令式编程和声明式编程,这是一个类似的概念。无论如何,请考虑以下代码段:
在Java中更新非UI线程的UI:
从C中的非UI线程更新UI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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的列表接口,检查异常作为合同失效的设计工具。考虑一个由数据库、文件系统或任何其他抛出检查异常的实现支持的列表。唯一可能的实现是捕获选中的异常并将其作为未选中的异常重新引发:
1 2 3 4 5 6 7 8 9 10 11 12
| @Override
public void clear ()
{
try
{
backingImplementation. clear();
}
catch (CheckedBackingImplException ex )
{
throw new IllegalStateException("Error clearing underlying list.", ex );
}
} |
现在你必须问,所有这些代码的意义是什么?选中的异常只会增加噪声,异常已被捕获但未被处理,合同设计(根据选中的异常)已被破坏。
结论
- 捕获异常与处理异常是不同的。
- 选中的异常会给代码添加噪声。
- 异常处理在没有它们的情况下在C中工作得很好。
我以前在博客上写过这个。
- 从C:—中的非UI线程更新UI—如果发生异常怎么办?
- 在这个例子中,Java和C都只是让异常在不处理它们的情况下传播(Java通过ILLealStaleExeExt)。区别在于,您可能希望处理FileNotFoundException,但处理InvocationTargetexception或InterruptedException将不太可能有用。
- 以C方式,我如何知道I/O异常可能发生?而且我也不会从运行中抛出异常…我认为这是滥用异常处理。抱歉,但对于代码的这一部分,我还可以看到您的一面。
- 已知的异常被放入C"javadoc"中;好的API将以这种方式定义所有已知的异常。您可能能够捕获run()中找不到的文件,但有些异常无法在那里处理。在run()中捕获它们,但处理不当并不好。也许我误解了。
- 我们正在努力做到这一点:-)因此,对于API的每个新版本,您必须梳理所有调用,并寻找可能发生的任何新异常?这很容易发生在公司内部的API中,因为它们不必担心向后兼容性。
- 你的意思是降低信噪比吗?
- 你在这里混东西。C语言比Java更为新近,对日常任务有更简单的设计。您可以在Java中实现相同的清洁设计,添加自己的库或实用程序。
- @在底层API的接口改变之后,ToSubeer不是被迫更新代码的吗?如果你在那里只有未检查的异常,你会在不知道的情况下以一个坏的/不完整的程序结束。
- @佛朗哥资产阶级,我认为这是他要说的。
- 我想这个答案很有说服力。"看看Java代码变得多么粗糙。Java已经冗长了!"不过,也有几句话:只是重新编写IOExceptions和InterruptedExceptions,然后像这样崩溃和烧掉是个坏主意。在每10个案例中,有9个案例中,有一种更负责任的方式来处理这种情况,而不是把它扫到地毯下。在哪里处理?你在这里做用户界面的工作,所以这里是正确的地方!如果在这两个片段中都做了这些改进,C版本就开始接近Java版本。…
- …所以,在本例中,我实际上声称检查过的异常实际上帮助您编写了更负责任的代码。InvocationTargetException只是不幸。很明显,它应该是一个RuntimeException。这是从早期的Java,没有人经历过恼怒检查异常给我们带来了今天。不过,我想强调的是,这是一个API设计问题,而不是作为语言特性的带有检查异常的问题。很难纠正吗?当然,有时候。但并不难,像这样的好特性应该被抛到窗外。
Artima发表了一篇对.NET的一位架构师AndersHejlsberg的采访,其中尖锐地涵盖了反对检查异常的论点。短品酒师:
The throws clause, at least the way it's implemented in Java, doesn't necessarily force you to handle the exceptions, but if you don't handle them, it forces you to acknowledge precisely which exceptions might pass through. It requires you to either catch declared exceptions or put them in your own throws clause. To work around this requirement, people do ridiculous things. For example, they decorate every method with,"throws Exception." That just completely defeats the feature, and you just made the programmer write more gobbledy gunk. That doesn't help anybody.
- 事实上,总建筑师。
- 我读过,对我来说,他的论点归结为"外面有坏的程序员"。
- 一点也不。关键是,很多时候你不知道如何处理被调用方法抛出的异常,而你真正感兴趣的情况甚至没有提到。打开一个文件,就会得到一个IO异常,例如…这不是我的问题,所以我把它扔了。但是顶级调用方法只想停止处理并通知用户存在未知问题。选中的异常根本没有帮助。这是可能发生的无数奇怪的事情之一。
- @是的,如果您不喜欢选中的异常,那么就执行一个"throw new runtimeexception"("在执行foo.bar()",e)并完成它。
- 当然,我认为他的真正论点是外面有人类。总的来说,使用检查异常所引起的疼痛比没有检查异常所引起的疼痛要小,这并不令人信服。
- @Thorbj&248;rnravandersen:这是正确的方法,但是我假设,如果一个方法既没有捕获到检查过的异常,也没有以某种方式指示它应该像现在一样渗透到堆栈中(即使Foo,它有一个throws WizzleException子句调用了Bar,它有一个匹配的子句,那么这样的包装应该隐式发生。不应该足够,因为很有可能,Bar的例外并不意味着与Foo的相同的事情。在90%的情况下应该做的事情的样板代码只是噪音。
- @在我的经验中,检查异常意味着您在开发过程的早期就考虑了错误处理。这会导致更高质量的代码(假定错误可以恢复)。
- "thorbj & # 248 rnravnandersen弱化;设计基础:Java,.NET是一copied,不幸的是,它使用了两种类型的异常是否是原发性deciding均值都在它应该是原发性的方法研究显示,与一般类型的东西是错误的,在事实的时候了,在这两个问题在很大程度上是正交的。重要的不是什么是错了,但是在什么状态的对象。此外,.NET和Java都是作用在默认情况下是异常和分辨率一般都是相同的东西,当他们再经常在不同的事实。
- supercat"的意思。这是elaborated的地方吗?
- "thorbj & # 248 rnravnandersen:不知道;我院;它只是我的意见。如果一个人的方法LoadDocument()抛出任何异常,这样,在处理来电的蛛网膜下腔出血(SAH)的文件是不是事实上的负载。如果该文件没有来电,可以生存的需求,未来的事情是无法知道是否有任何试图加载的文件的危险的副作用,没有不反卷。没有什么说的是异常的类型。如果一个失败的尝试在负载文件的用户侧的影响,informing它找不到足够的负载可以处理的异常。
- "# 248 thorbj &:;如果试图rnravnandersen负载损坏的文件的日期是与其他开放式结构,共享的文件,用户的问题informing可能是适当的,但不应该被视为一个时间的问题。不幸的是,Java和.NET都认为这将是解决catch块就任何异常的代码手动rethrows除非它。
- 在这LoadDocument()@ supercat案例的方法有太多的责任。异常处理的多,应该是在低水平,而不是被允许中断的操作到共享数据结构。描述的是一个糟糕的设计,简单的就是你没有故障和异常的机制。
- 他对操作系统的arguing坏程序员自己检查,没有异常。只让那些不具有异常检查坏的程序员,甚至更糟。
有效的Java异常可以很好地解释何时使用未选中的以及何时使用已检查的异常。以下是该文章的一些引言,以突出重点:
Contingency:
An expected condition demanding an alternative response from a method that can be expressed in terms of the method's intended purpose. The caller of the method expects these kinds of conditions and has a strategy for coping with them.
Fault:
An unplanned condition that prevents a method from achieving its intended purpose that cannot be described without reference to the method's internal implementation.
(因此不允许使用表,因此您可能希望从原始页中读取以下内容…)
Contingency
-
Is considered to be: A part of the design
-
Is expected to happen: Regularly but rarely
-
Who cares about it: The upstream code that invokes the method
-
Examples: Alternative return modes
-
Best Mapping: A checked exception
Fault
-
Is considered to be: A nasty surprise
-
Is expected to happen: Never
-
Who cares about it: The people who need to fix the problem
-
Examples: Programming bugs, hardware malfunctions, configuration mistakes,
missing files, unavailable servers
-
Best Mapping: An unchecked exception
- 我知道什么时候使用它们,我想知道为什么那些不听从建议的人…不要听从那个建议:—)
- 因为人就是人?
- 什么是编程错误,以及如何将它们与使用错误区分开来?如果用户将错误的参数传递给程序,这是一个编程错误吗?从Java的角度来看,它可能不是编程错误,但是从shell脚本的角度来看,它是一个编程错误。那么,args[]中的无效参数是什么?它们是偶然事件还是故障?
- @ TouBube——因为Java库设计器选择了各种不可恢复的低级别故障作为检查异常,而这些异常显然是未经检查的。例如,FileNotFound是唯一应该检查的IOException。对于JDBC——只连接到数据库,可以合理地认为是一种意外情况。所有其他的sqlExceptions都应该是失败的,并且未选中。错误处理应该正确地处于"业务"或"请求"级别——请参阅"及早抛出,晚捕获"最佳实践。已检查的异常是一个障碍。
- 你的论点只有一个巨大的缺陷。""应急"不能通过异常处理,而是通过业务代码和方法返回值处理。例外是指,正如这个词所说的,特殊情况,因此错误。
- @Matteomosca错误返回代码往往会被忽略,这足以取消它们的资格。实际上,任何不寻常的事情通常只能在堆栈中的某个地方处理,这是异常的用例。我可以想象类似于File#openInputStream返回Either的东西——如果这就是你的意思,那么我们可以同意。
- @马阿蒂努斯,这正是我的意思。
最初我同意你的看法,因为我一直支持检查异常,并开始思考为什么我不喜欢在.NET中不检查异常。但后来我意识到我不会像检查过的异常那样犯错。
回答你的问题,是的,我喜欢我的程序显示堆栈跟踪,最好是真正丑陋的。我希望应用程序爆炸成一堆可怕的错误消息,这些错误消息是您所能看到的。
原因是,如果它这样做了,我必须修复它,我必须马上修复它。我想马上知道有问题。
您实际处理异常的次数是多少?我说的不是捕获异常——我说的是处理异常?写以下内容太容易了:
1 2 3 4 5
| try {
thirdPartyMethod();
} catch(TPException e) {
// this should never happen
} |
我知道你可以说这是不好的做法,"答案"是做一些例外的事情(让我猜猜,记录下来?)但是在现实世界中,大多数程序员都不这样做。
所以,是的,如果我不需要这样做的话,我不想捕获异常,我希望我的程序在我出错的时候爆炸得非常厉害。默默无闻的失败是最糟糕的结果。
- Java鼓励你做这种事情,这样你就不必对每一个方法签名添加任何类型的异常。
- 我处理这个异常,在catch中的代码占100%的时间。代码不仅仅记录了异常不太常见的事实。大多数情况下,代码会抛出不同的异常类型。
- 滑稽的…自从我正确地接受了检查过的异常并正确地使用了它们之后,我的程序就不再在你脸上堆满了客户的不满。如果在开发过程中您有一个丑陋的坏堆栈跟踪,那么客户也一定会得到它们。D'love to see his face when he sees arrayindexoutofboundsException with a mile high stack trace on his crashed system instead that a little tray notification that the color config for button xyz cannot be parsed so the default was used with the software happy humming along当他看到arrayindexoutofbound
- 也许Java所需要的是一个"ChanthDLE"声明,它指定一个方法或尝试/ catch块代码不准备处理它里面发生的一个特定异常,并且通过该方法(而不是一个被叫方法)中的一个显式抛出而发生的任何此类异常都应该被自动包装。在RuntimeException中重新调用。imho,选中的异常在没有被包装的情况下很少向上传播调用堆栈。
- @Newtopian——我写的服务器和高可靠性软件已经有25年了。我的程序从未崩溃过,我使用的是高可用性、重试和重新连接、基于集成的金融和军事系统。我有绝对客观的基础来选择运行时异常。检查异常会使遵循正确的"早抛、晚抓"最佳实践更加困难。正确的可靠性和错误处理处于"业务"、"连接"或"请求"级别。(或在分析数据时偶尔)。选中的异常妨碍了正确的操作。
- 你在这里谈论的例外是RuntimeExceptions,实际上你不必去了解它,我同意你应该让程序爆炸。您应该始终捕获和处理的异常是经过检查的异常,如IOException。如果你得到了一个IOException,你的代码中就没有什么可以修复的了;你的程序不应该仅仅因为网络出了故障而崩溃。
- 是否有一个静态分析工具可以找到空的异常处理程序并警告程序员,或者一些IDE已经这样做了?
简而言之:
例外是一个API设计问题。--不多不少。
选中异常的参数:
为了理解为什么选中的异常可能不是好事,让我们把问题转过来问:什么时候或者为什么选中的异常有吸引力,也就是说,为什么您希望编译器强制声明异常?
答案是显而易见的:有时您需要捕获一个异常,并且只有当被调用的代码为您感兴趣的错误提供了一个特定的异常类时,这才是可能的。
因此,检查异常的理由是编译器强制程序员声明抛出哪些异常,并且希望程序员随后也记录特定的异常类和导致它们的错误。
但实际上,常常一个包com.acme只抛出一个AcmeException,而不是特定的子类。然后,呼叫者需要处理、声明或重新发送AcmeExceptions信号,但仍然无法确定是发生了AcmeFileNotFoundError还是AcmePermissionDeniedError信号。
因此,如果您只对AcmeFileNotFoundError感兴趣,那么解决方案是向acme程序员提交一个特性请求,并告诉他们实现、声明和记录AcmeException的子类。
那为什么还要麻烦呢?
因此,即使有检查过的异常,编译器也不能强制程序员抛出有用的异常。这仍然只是API的质量问题。
因此,没有检查异常的语言通常不会变得更糟。程序员可能会倾向于抛出一般Error类而不是AcmeException类的非特定实例,但如果他们关心他们的API质量,他们终究会学会引入AcmeFileNotFoundError。
总的来说,异常的规范和文档与一般方法的规范和文档没有太大的区别。这些也是一个API设计问题,如果程序员忘记实现或导出一个有用的特性,那么需要改进API,以便您能够有效地使用它。
如果你遵循这一推理,很明显,在Java等语言中如此常见的异常声明、捕获和重新抛出的"麻烦"通常没有什么价值。
还值得注意的是,Java VM没有检查异常——只有Java编译器检查它们,并且在运行时具有变化的异常声明的类文件是兼容的。Java VM安全性没有通过检查异常而改进,只有编码风格。
- 你对本身的argues参数。"有时候,如果你需要捕捉异常,往往是穷人和API质量检查,没有异常,你不会知道这是否对设计文件按一定的比例neglected抛出异常的方法是需要被捕获。这不是偶与AcmeFileNotFoundError和AcmeException抛出你的好运气figuring是错误的和你在哪里需要捕捉它。检查异常modicum程序员提供的保护对糟糕的API设计。
- Java库设计制造的严重的错误。"检查的异常是在可预见的recoverable contingencies &;-如发现文件没有,失败的连接。他们的意思是不适合用于低电平或系统故障。它也有一个精细强行开文件被检查,但没有增强或恢复重试失败的是单字节写重试运行SQL查询等。正确地处理或恢复是在"商业"或"请求"的水平,这使pointlessly检查异常困难。literatejava.com /例外/ & hellip;
在过去的三年中,我一直与几个开发人员在相对复杂的应用程序中合作。我们有一个代码库,它经常使用经过检查的异常,并进行适当的错误处理,而其他一些则没有。
到目前为止,我发现使用带有检查异常的代码库更容易。当我使用其他人的API时,当我调用代码并通过日志记录、显示或忽略(是的,有忽略异常的有效案例,例如类加载器实现)正确地处理代码时,我可以准确地看到我所期望的错误条件。这给了我编写的代码一个恢复的机会。我传播的所有运行时异常直到它们被缓存并用一些通用错误处理代码处理。当我发现一个我不想在特定级别上处理的已检查异常,或者我认为是一个编程逻辑错误时,我将它包装成一个runtimeexception并让它冒泡。永远不要在没有充分理由的情况下接受例外情况(这样做的充分理由是相当少的)
当我使用没有检查异常的代码库时,它会让我有点难以提前知道调用函数时会发生什么,这会严重破坏一些东西。
这当然是一个偏好和开发人员技能的问题。编程和错误处理的两种方法都同样有效(或无效),所以我不会说只有一种方法。
总之,我发现使用检查异常更容易,特别是在有很多开发人员的大型项目中。
- 我愿意。对我来说,它们是合同的重要组成部分。不必在API文档中详细介绍,我就可以快速了解最有可能出现的错误场景。
- 同意。当我尝试进行网络呼叫时,我曾在.NET中经历过检查异常的必要性。由于知道网络故障随时都可能发生,我不得不通读API的整个文档,找出在这种情况下我需要特别注意的异常情况。如果C检查了异常情况,我会立刻知道的。其他的C开发者可能只会让应用程序因为一个简单的网络错误而崩溃。
例外类别
在谈到例外的时候,我总是参考埃里克·利珀特的恼人的例外博客文章。他将例外情况分为以下几类:
- 致命的-这些例外不是你的错:你不能阻止,你不能明智地处理它们。例如,OutOfMemoryError或ThreadAbortException。
- 这些异常是你的错:你应该阻止它们,它们在你的代码中代表错误。例如,ArrayIndexOutOfBoundsException、NullPointerException或任何IllegalArgumentException。
- 烦人-这些例外并不例外,不是你的错,你不能阻止它们,但你必须处理它们。它们通常是错误设计决策的结果,例如从Integer.parseInt中抛出NumberFormatException,而不是提供Integer.tryParseInt方法,该方法在解析失败时返回布尔值false。
- 外生的-这些例外通常是例外的,不是你的错,你不能(合理地)阻止它们,但你必须处理它们。例如,FileNotFoundException。
API用户:
- 不能处理致命或无效的异常。
- 应该处理恼人的异常,但它们不应该出现在理想的API中。
- 必须处理外部异常。
已检查异常
API用户必须处理特定异常的事实是调用方和被调用方之间的方法契约的一部分。约定指定了以下内容:被调用者期望的参数的数量和类型、调用者期望的返回值的类型以及调用者期望处理的异常。
因为vexing异常不应该存在于API中,所以只有这些外部异常必须被检查为方法契约的一部分。相对较少的异常是外生的,所以任何API都应该有相对较少的检查异常。
选中的异常是必须处理的异常。处理异常可能和吞咽异常一样简单。那里!处理异常。时期。如果开发人员想用这种方式处理,那就好。但他不能忽视这一例外,并受到警告。
API问题
但是任何检查过烦人和致命异常(如JCL)的API都会给API用户带来不必要的压力。必须处理此类异常,但要么异常非常常见,以至于在第一时间不应该是异常,要么在处理异常时什么也做不到。这导致Java开发人员讨厌被检查的异常。
此外,许多API没有适当的异常类层次结构,导致各种非外部异常都由一个检查过的异常类(如IOException)表示。这也导致Java开发人员讨厌被检查的异常。
结论
外生的例外是那些不是你的错,不可能被阻止,应该被处理的。这些构成了可以抛出的所有异常的一小部分。API应该只检查外部异常,而不检查所有其他异常。这将使API变得更好,减少对API用户的压力,从而减少捕获所有、吞咽或重新引发未检查异常的需要。
所以不要讨厌Java和它的检查异常。相反,讨厌过度使用检查异常的API。
- 并通过没有层次结构来滥用它们。
- filenotfound和建立JDBC/网络连接是偶然事件,可以检查异常,因为这些异常是可预测的和(可能的)可恢复的。大多数其他IOException、SQLExceptions、RemoteException等都是不可预测的和不可恢复的失败,应该是运行时异常。由于错误的Java库设计,我们都集中在这个错误和现在主要是使用Spring和Hibernate(谁得到了他们的设计权)。
- 您通常应该处理BoneHead异常,尽管您可能不希望将其称为"处理"。例如,在Web服务器中,我记录它们并向用户显示500。由于这个异常是出乎意料的,所以在修复bug之前,我可以做的就只有这些了。
实际上,检查异常一方面增加了程序的健壮性和正确性(您被迫对接口进行正确的声明——方法抛出的异常基本上是一种特殊的返回类型)。另一方面,您面临的问题是,由于异常"冒泡",当您更改一个方法引发的异常时,通常需要更改很多方法(所有调用方和调用方的调用方,等等)。
Java中的检查异常不能解决后一个问题;C和VB.NET用洗澡水把婴儿扔掉。
Oopsla2005论文(或相关技术报告)中描述了一种走中间道路的好方法。
简而言之,它允许您说:method g(x) throws like f(x),这意味着g抛出所有抛出的异常f。瞧,检查了异常,没有级联更改问题。
尽管这是一篇学术论文,我还是鼓励你阅读(部分)它,因为它很好地解释了检查异常的好处和缺点。
好啊。。。检查异常并不理想,有一些警告,但它们确实有一定的用途。在创建API时,有一些特定的失败案例属于此API的契约。如果在一个强静态类型的语言(如Java)的上下文中,如果不使用已检查的异常,则必须依靠自组织文档和约定来传达错误的可能性。这样做会消除编译器在处理错误时带来的所有好处,而您将完全听从程序员的意愿。
因此,删除检查过的异常(如在C中完成的异常),那么如何在编程和结构上传递错误的可能性呢?如何通知客户机代码可能会发生此类错误,并且必须加以处理?
我听说在处理选中的异常时有各种各样的恐怖,它们被误用了这是肯定的,但未选中的异常也是如此。我说,等几年,当API被堆得很深的时候,你会乞求返回某种结构化的方法来传递失败。
举个例子,当异常被抛出到API层底部的某个地方并冒泡了,因为没有人知道这个错误甚至有可能发生,尽管这是一种错误类型,当调用代码抛出它时是非常合理的(例如,fileNotFoundException,而不是vogonstrashingarTheException…在这种情况下,不管我们是否处理它都无关紧要,因为没有什么可以处理的了)。
许多人认为,无法加载文件几乎总是这个过程的世界末日,它必须是一个可怕而痛苦的死亡。所以是的…当然。。。好啊。。您为某个东西构建了一个API,它在某个时刻加载文件…我作为上述API的用户只能响应…"你到底是谁来决定我的程序什么时候会崩溃!"当然,考虑到异常被吞噬而不留下任何痕迹的选择,或是比玛丽安娜沟更深的叠加痕迹的eletroflabingchunkfluxmanifoldchuggingexception,我会毫不犹豫地选择后者,但这是否意味着这是处理异常的理想方式?我们能不能不在中间的某个地方,每次异常遍历到一个新的抽象级别时,它都会被重铸和包装,这样它实际上就意味着什么呢?
最后,我看到的大多数论点是"我不想处理异常,很多人不想处理异常"。检查异常迫使我去处理它们,因此我讨厌检查异常,"完全消除这种机制,把它放任到地狱的裂口,这是愚蠢的,缺乏灵活性和远见。
如果我们消除检查的异常,我们也可以消除函数的返回类型,并始终返回"anytype"变量…那会让生活变得简单多了,不是吗?
- 如果有一种声明性方法表示块中的任何方法调用都不会引发某些(或任何)已检查的异常,则选中的异常将非常有用,并且任何此类异常都应自动包装并重新引发。如果对声明为抛出检查异常的方法的调用以调用速度/返回为异常处理速度进行交换(这样预期的异常处理速度几乎可以与正常程序流一样快),则它们可能更有用。不过,目前这两种情况都不适用。
问题
对于异常处理机制,我看到的最糟糕的问题是它在很大程度上引入了代码复制!让我们诚实一点:在95%的时间里,开发人员真正需要做的就是以某种方式将其传达给用户(在某些情况下,也包括传达给开发团队,例如通过发送带有堆栈跟踪的电子邮件)。因此,通常在处理异常的每个地方使用相同的代码行/代码块。
假设我们在每个catch块中为某种类型的已检查异常执行简单的日志记录:
1 2 3 4 5
| try{
methodDeclaringCheckedException();
}catch(CheckedException e){
logger.error(e);
} |
如果这是一个常见的异常,那么在一个更大的代码库中甚至可能有几百个这样的try-catch块。现在假设我们需要引入基于弹出对话框的异常处理,而不是控制台日志记录,或者开始向开发团队额外发送电子邮件。
稍等片刻。。。我们真的要编辑代码中的数百个位置吗?!你明白我的意思:—)。
解决方案
我们对解决这个问题所做的是引入异常处理程序(我将进一步称为eh)的概念,以集中化异常处理。对于每个需要处理异常的类,依赖项注入框架都会注入异常处理程序的实例。因此,异常处理的典型模式如下:
1 2 3 4 5
| try{
methodDeclaringCheckedException();
}catch(CheckedException e){
exceptionHandler.handleError(e);
} |
现在,为了定制我们的异常处理,我们只需要在一个地方更改代码(eh代码)。
当然,对于更复杂的情况,我们可以实现几个EHS子类,并利用DI框架提供的特性。通过更改DI框架配置,我们可以轻松地在全局范围内切换eh实现,或者将eh的特定实现提供给具有特殊异常处理需求的类(例如使用guice@named annotation)。
这样,我们就可以在应用程序的开发和发布版本中区分异常处理行为(例如,开发—记录错误并停止应用程序,生成—以更详细的信息记录错误并让应用程序继续执行),而无需付出任何努力。
最后一件事
最后但并非最不重要的是,似乎可以通过将异常"向上"传递到某个顶级异常处理类来实现相同的集中化。但这导致了代码和方法签名的混乱,并引入了这个线程中其他人提到的维护问题。
- 例外是为了对它们做一些有用的事情而发明的。将它们写入日志文件或呈现一个漂亮的窗口是没有用的,因为原始问题不能通过这种方法得到解决。做一些有用的事情需要尝试不同的解决方案策略。示例:如果我无法从服务器A获取数据,我将在服务器B上进行尝试。或者如果算法A产生堆溢出,我将尝试速度慢得多但可能成功的算法B。
- @塞文是的,理论上都是好的和真的。但现在让我们继续练习单词。请诚实地回答,你在实际的文字项目中多久做一次?在这个实际项目中,catch块的哪个部分对exceptin做了一些真正"有用"的事情?10%是好的。生成异常的常见问题包括尝试从不存在的文件中读取配置、OutOfMemoryErrors、NullPointerExceptions、数据库约束完整性错误等。是否确实尝试从所有这些错误中优雅地恢复?我不相信你:)。通常没有办法恢复。
- @piotrsobczyk:如果一个程序由于一个suer请求而采取了一些行动,并且操作以某种方式失败,并且没有破坏系统状态中的任何东西,那么通知用户操作无法完成是处理这种情况的一种非常有用的方法。C和.NET中异常的最大缺点是没有一致的方法来确定系统状态中的任何东西是否可能已经损坏。
- 对,@piotrsobczyk.在大多数情况下,对异常作出响应的唯一正确操作是回滚事务并返回错误响应。"解决异常"的想法意味着我们没有(也不应该)的知识和权威,并且违反了封装。如果我们的应用程序不是数据库,我们不应该尝试修复数据库。彻底失败并避免写入错误数据(ceving)就足够有用了。
- @Piotrsobczyk昨天,我处理了一个"无法读取对象"的异常(这只会发生因为基础数据库在软件之前已经更新了-这不应该发生,但可能是由于人为错误),通过故障切换到保证指向对象旧版本的数据库的历史版本。
安德斯在《软件工程无线电》第97集中谈到了检查过的异常的陷阱,以及为什么把它们排除在C之外的原因。
试图解决未回答的问题:
If you throw RuntimeException subclasses instead of Exception subclasses then how do you know what you are supposed to catch?
这个问题包含了似是而非的推理。仅仅因为API告诉你它抛出了什么并不意味着你在所有情况下都用同样的方式处理它。换一种说法,您需要捕获的异常因使用抛出异常的组件的上下文而异。
例如:
如果我正在为数据库编写连接测试程序,或者为检查用户输入的xpath的有效性而编写一些东西,那么我可能希望捕获并报告操作引发的所有已检查和未检查的异常。
但是,如果我正在编写一个处理引擎,我可能会像处理NPE一样处理xpathexception(选中):我会让它运行到工作线程的顶部,跳过该批的其余部分,记录问题(或将其发送到支持部门进行诊断),并为用户留下反馈以联系支持人员。
- 确切地。简单明了,异常处理的方式。正如Dave所说,正确的异常处理通常是在较高的级别上完成的。""早抛,晚抓"是原则。经过检查的例外情况使这变得困难。
正如人们已经说过的,在Java字节码中不存在检查异常。它们只是一个编译器机制,与其他语法检查不同。我看到检查过的异常非常像编译器抱怨冗余条件:if(true) { a; } b;。这很有帮助,但我可能是故意这样做的,所以让我忽略你的警告。
事实上,如果你强制执行检查过的异常,那么你就不能强迫每个程序员"做正确的事情",而其他人现在都是附带的损害,他们只是因为你制定的规则而恨你。
修复那里的坏程序!不要试图修复语言以不允许它们!对于大多数人来说,"做一些关于异常的事情"实际上就是告诉用户它。我也可以告诉用户一个未选中的异常,所以请将您选中的异常类从我的API中删除。
- 对,我只是想强调无法访问的代码(会产生错误)和具有可预测结果的条件之间的区别。稍后我将删除此评论。
我在c2.com上写的东西与它原来的形式基本上没有变化:a href="http://c2.com/cgi/wiki?"检查异常与可见模式不兼容">检查异常与可见模式不兼容/aa
综上所述:
访问者模式及其关联是一类接口,其中间接调用方和接口实现都知道异常,但接口和直接调用方形成了一个不知道的库。
checkedExceptions的基本假设是所有声明的异常都可以从使用该声明调用方法的任何点抛出。VisitorPattern揭示了这个假设是错误的。
在这种情况下,检查异常的最终结果是大量无用的代码,这些代码基本上在运行时删除编译器的检查异常约束。
至于根本问题:
我的一般想法是顶级处理程序需要解释异常并显示适当的错误消息。我几乎总是看到IO异常、通信异常(出于某种原因,API可以区分)或任务致命错误(程序错误或备份服务器上的严重问题),所以如果我们允许对严重的服务器问题进行堆栈跟踪,这就不太难了。
- 在接口中应该有类似dagnodeexception的东西,然后捕获IOexception并将其转换为dagnodeexception:public void call(dagnode arg)throws dagnodeexception;
- @托弗,这正是我的观点。我发现不断包装和展开异常比移除检查过的异常更糟糕。
- 我们完全不同意…但是您的文章仍然没有回答真正的底层问题,即当抛出运行时异常时,如何阻止应用程序向用户显示堆栈跟踪。
- @tofubeer——当失败时,告诉用户失败是正确的!除了用"空"或不完整/不正确的数据"覆盖"故障之外,您还有什么选择?假装成功是谎言,这只会让事情变得更糟。在高可靠性系统方面有25年的经验,只有在适当的情况下才应谨慎使用重试逻辑。我也希望访问者可能再次失败,无论您重试多少次。除非你在驾驶飞机,否则换用同一算法的第二个版本是不切实际和不可信的(而且可能会失败)。
这篇文章是我读过的Java中异常处理的最好的一篇文章。
它支持未经检查的异常,但这种选择的解释非常仔细,并且基于强有力的论据。
我不想在这里引用太多的文章内容(最好把它作为一个整体来阅读),但它涵盖了这个主题中未经检查的异常倡导者的大多数论点。尤其是这个论点(似乎很流行)包括:
Take the case when the exception was thrown somewhere at the bottom of the API layers and just bubbled up because nobody knew it was even possible for this error to occur, this even though it was a type of error that was very plausible when the calling code threw it (FileNotFoundException for example as opposed to VogonsTrashingEarthExcept... in which case it would not matter if we handle it or not since there is nothing left to handle it with).
作者"回应":
It is absolutely incorrect to assume that all runtime exceptions
should not be caught and allowed to propagate to the very"top" of the
application. (...) For every exceptional condition that is required to
be handled distinctly - by the system/business requirements -
programmers must decide where to catch it and what to do once the
condition is caught. This must be done strictly according to the
actual needs of the application, not based on a compiler alert. All
other errors must be allowed to freely propagate to the topmost
handler where they would be logged and a graceful (perhaps,
termination) action will be taken.
主要思想或文章是:
When it comes to error handling in software, the only safe and correct assumption that may ever be made is that a failure may occur in absolutely every subroutine or module that exists!
因此,如果"没有人知道这个错误可能发生",那么这个项目就有问题。如作者建议的,此类异常应至少由最通用的异常处理程序(例如,处理所有未由更具体的处理程序处理的异常的处理程序)处理。
如此可悲,似乎没有多少人发现这篇伟大的文章。我全心全意推荐那些犹豫不决的人,哪种方法更适合花些时间来阅读。
检查过的异常,在其最初的形式中,是一种处理意外事件而不是失败的尝试。值得称赞的目标是突出特定的可预测点(无法连接、找不到文件等),并确保开发人员处理了这些问题。
最初的概念中从未包括的是强制宣布大量的系统性和不可恢复的故障。这些失败永远不会被声明为检查异常。
代码中通常可能出现故障,EJB、Web&Swing/AWT容器已经通过提供最外层的"失败请求"异常处理程序来解决这一问题。最基本的正确策略是回滚事务并返回错误。
一个关键点是,运行时和检查的异常在功能上是等效的。检查的异常无法处理或恢复,运行时异常无法处理或恢复。
反对"已检查"异常的最大理由是大多数异常无法修复。简单的事实是,我们不拥有破坏的代码/子系统。我们看不到实现,我们对它不负责,也无法修复它。
如果我们的应用程序不是数据库…我们不应该尝试修复数据库。这将违反封装原则。
特别有问题的是JDBC(SQLException)和RMI for EJB(RemoteException)的领域。这些强制普遍存在的系统可靠性问题,实际上不是可修复的,而不是根据最初的"检查异常"概念来确定可修复的突发事件。
Java设计中的另一个严重缺陷是异常处理应该正确地放置在尽可能高的"业务"或"请求"级别。这里的原则是"早抛,晚抓"。选中的异常几乎没有做什么,但会妨碍这一点。
我们在Java中有一个明显的问题,即需要数千个无需尝试的catch块,其中相当大的比例(40% +)被错误编码。几乎所有这些都没有实现任何真正的处理或可靠性,但是会增加主要的编码开销。
最后,"检查的异常"与fp函数编程非常不兼容。
他们对"立即处理"的坚持与"延迟捕获"异常处理最佳实践以及抽象循环/或控制流的任何FP结构都不一致。
许多人谈论"处理"检查过的异常,但他们是在自言自语。在出现错误(数据为空、不完整或不正确)后继续,以假装成功,这不处理任何事情。这是最低形式的工程/可靠性弊端。
彻底失败是处理异常最基本的正确策略。回滚事务、记录错误以及向用户报告"失败"响应都是合理的做法——最重要的是,防止将不正确的业务数据提交到数据库中。
异常处理的其他策略是在业务、子系统或请求级别上的"重试"、"重新连接"或"跳过"。所有这些都是一般的可靠性策略,并且在运行时异常情况下工作得很好/更好。
最后,与使用不正确的数据运行相比,失败要好得多。如果继续,将导致次要错误,与原始原因相距较远,更难调试,或者最终导致提交错误数据。人们为此被炒鱿鱼。
见:-http://literatejava.com/exceptions/checked-exceptions-javas-most-error/
- 这是一个很好的答案,但它没有解决我问题的核心:你如何知道如何捕获异常并回滚?或者你只是说人们应该在每件事情的顶层捕获runtimeexception,以确保代码不会崩溃?
- @为了提交您的请求(Web)或事件/操作(GUI)处理程序,本质上是处理业务操作的,并且始终有可能发生内部/基础结构故障。失败应该在业务级别通过回滚来处理——正如您所说,使用可理解的错误消息。此类"可靠性处理程序"catch子句通常应捕获"exception"以捕获任何运行时/或检查的异常。故障的形式/类型与处理基本无关,除非在极少数情况下发生特定的已知意外事件,并且存在从中恢复的逻辑。
- 您不应该捕获导致特别可怕的代码的异常,这些代码可以将合法的失败(如arrayindexoutofbounds)隐藏在代码之外。在本质上,你可以通过做你建议的事情来轻易地隐藏bug。
- 我的观点是,作为总的战略,正确地失败。未选中的异常有助于避免强制插入catch块。捕获和错误日志可以留给几个最外层的处理程序,而不是在整个代码库中错误地编码数千次(这实际上是隐藏错误的原因)。对于任意失败,未检查的异常绝对是最正确的。意外事件——如资金不足等可预测的结果——是唯一合法值得检查的例外。
- 托马斯·W,这仍然没有解决外层如何知道要捕捉什么的问题,这是根本问题,也没有解决外层如何知道要捕捉什么的问题,这是根本问题。
- 我的回答已经解决了这个问题。首先,1)最外层的故障处理程序应该捕获所有内容。除此之外,仅对于特定的已识别站点,2)可以捕获和处理特定的预期意外事件——在它们被抛出的即时站点上。这意味着找不到文件,资金不足等,这些可以从-没有更高。封装原理意味着外层不能/不应该负责理解/从深层次的故障中恢复。第三,3)其他一切都应该向外抛——如果可能的话,不加检查。
- 最外层的处理程序捕获异常,记录异常,并返回"失败"响应或显示错误对话框。非常简单,一点也不难定义。要点是,由于封装原则,每个不立即和局部可恢复的异常都是不可恢复的失败。如果要知道的代码无法恢复它,那么整个请求将彻底失败。这是正确的方法。
- 非常简单和非常危险-你最终会以这种方式捕获真正的虫子。异常捕获RuntimeException,RuntimeException指示程序员错误。
- 不正确。最外层处理程序的任务是在"请求"边界上彻底失败并记录错误。中断的请求正确失败,报告异常,线程可以继续服务下一个请求。这种最外层处理器是Tomcat、AWT、施普灵河、EJB容器和Java"主"线程中的标准特征。
- 我怎么了?异常捕获RuntimeException。runtimeexception的目的是指示程序员错误。
- 为什么在请求边界或最外面的处理程序报告"真正的错误"是危险的????我经常在系统集成和可靠性领域工作,在那里正确的可靠性工程实际上很重要,并使用"未检查的异常"方法来实现这一点。我真的不确定你到底在争论什么——看起来你可能真的想以不受限制的例外方式度过3个月,感受一下,然后也许我们可以进一步讨论。谢谢。
- arrayindexoutofbounds异常是一个不应捕获的示例。在系统集成和处理未知的第三方代码方面,那么,是的,您必须处理不可预见的事情,但这个问题是关于丢弃它们而不是捕获它们。
- @我当然想抓住所有的例外,包括任何地方。这是一个编程错误,必须报告进行修复,但绝对没有理由完全忽略它。抓到EDOCX1[0]没有危险;相反,它必须在某个地方完成,以便向用户展示500,并向开发人员报告错误。所以在我的代码中(当然在Spring和其他框架中)有一个类似于try {...} catch (RuntimeException e) {...} catch (Exception e) {...} 的外部块。
这不是一个反对纯检查异常概念的论点,但是Java类使用的类层次结构是一个怪异的展示。我们总是简单地称之为"异常"——这是正确的,因为语言规范也这样称呼它们——但是在类型系统中如何命名和表示异常?
按类Exception一个想象?不,因为Exception是例外,同样的例外是Exceptions,除了那些不是Exceptions的例外,因为其他例外实际上是Errors,这是另一种例外,一种不应该发生的额外例外,除非它发生,你应该除非有时你不得不这样做。但这并不是全部,因为您还可以定义其他既不是Exception也不是Error的异常,而只是Throwable的异常。
哪些是"选中"的例外情况?Throwable是检查异常,除非它们也是Errors,这是未检查异常,然后还有Exceptions,这也是Throwables,是检查异常的主要类型,但也有一个例外,那就是如果它们也是RuntimeExceptions,因为这是另一种类型的unche禁止异常。
RuntimeException是干什么用的?正如这个名字所暗示的,它们是例外,就像所有的Exceptions一样,它们发生在运行时,就像所有的例外一样,实际上,除了RuntimeExceptions与其他运行时Exceptions相比是例外,因为它们不应该发生,除非你犯了一些愚蠢的错误,尽管RuntimeExceptions从来不是Errors。S,所以它们是为了那些异常错误但实际上不是ErrorS的东西。除了RuntimeErrorException外,它实际上是RuntimeException对于ErrorS的东西。但是,所有的异常都不应该代表错误的情况吗?是的,所有的。除ThreadDeath外,这是一个例外的非例外情况,因为文件解释说这是一个"正常现象",这就是为什么他们把它变成Error的类型。
不管怎样,由于我们将所有异常划分为Errors(对于异常执行异常,未选中)和Exceptions(对于较少的异常执行错误,除非没有异常执行错误,否则已选中),我们现在需要两种不同类型的异常。所以我们需要IllegalAccessError和IllegalAccessException、InstantiationError和InstantiationException、NoSuchFieldError和NoSuchFieldException、NoSuchMethodError和NoSuchMethodException、ZipError和ZipException。
但是,即使在检查异常时,也总是有(相当容易)的方法欺骗编译器并在不检查异常的情况下抛出它。如果你这样做,你可以得到一个UndeclaredThrowableException,除非在其他情况下,它可以作为一个UnexpectedException,或一个UnknownException(与UnknownError无关,只是为了"严重的例外"),或一个ExecutionException,或一个InvocationTargetException或一个ExceptionInInitializerError。
哦,而且我们不能忘记Java 8的新的EDCOX1×44,这是一个EDCOX1×12的例外,旨在通过将由I/O错误引起的检查EDCOX1 46个异常(它不会引起EDCOX1,47个例外),将异常检查概念扔出窗口,尽管这也存在异常,这是非常难掌握的。所以你不需要检查它们。
谢谢JAVA!
- 就我所说,这个答案只是说"Java的异常是一团糟",充满讽刺意味,可笑的方式。它似乎没有做的是解释为什么程序员倾向于避免试图理解这些东西是如何工作的。另外,在现实生活中(至少是我有机会处理的那些),如果程序员不刻意让他们的生活变得更艰难,那么异常就没有你所描述的那么复杂了。
检查异常的一个问题是,即使接口的一个实现使用异常,也常常将异常附加到接口的方法上。
检查异常的另一个问题是它们往往被误用。这一点的完美例子是在java.sql.Connection的close()方法中。它可以抛出一个SQLException,即使您已经明确地声明您已经完成了连接。什么信息可以关闭()可能表示您关心的?
通常,当我关闭()一个connection*时,它看起来是这样的:
1 2 3 4 5
| try {
conn. close();
} catch (SQLException ex ) {
// Do nothing
} |
另外,不要让我开始了解各种分析方法和NumberFormatException…NET的TyPARSE,它不会抛出异常,使用起来很容易,要返回Java是很痛苦的(我们使用Java和C i工作的地方)。
*作为附加注释,pooledConnection的connection.close()甚至不会关闭连接,但您仍然需要捕获sqlException,因为它是一个选中的异常。
- 但是,mysql驱动程序(例如)可以在close中引发sqlException…
- 对,任何司机都可以…问题是"为什么程序员应该关心?"因为他已经访问了数据库。文档甚至警告您,在调用close()之前,应该始终提交()或回滚()当前事务。
- 许多人认为关闭文件不能引发异常…stackoverflow.com/questions/588546/&hellip;您是否100%确定没有任何情况会有影响?
- 我百分之百地肯定,没有什么情况会有关系,打电话的人不会尝试/接住。
- 很好的例子,关闭连接,马丁!我只能换句话给你:如果我们只是明确地说,我们已经完成了一个连接,为什么当我们关闭它的时候,应该去麻烦发生了什么。有更多这样的情况,程序员并不真正关心异常是否发生,他完全正确。
- @piotrsobczyk:如果在启动事务后关闭连接,但既不确认也不回滚,则某些SQL驱动程序将发出尖叫。唉,大声喧哗总比默默地忽略问题要好,至少在这样的情况下,大声喧哗不会导致其他异常丢失。
程序员需要知道一个方法可能抛出的所有异常,以便正确地使用它。因此,仅仅用一些异常来殴打他并不一定能帮助粗心的程序员避免错误。
这种微小的好处被繁重的成本所抵消(尤其是在更大、更不灵活的代码库中,不断修改接口签名是不现实的)。
静态分析可以很好,但真正可靠的静态分析通常需要程序员严格的工作。有一个成本效益计算,需要为导致编译时错误的检查设置高的条。如果IDE承担起通信的角色(包括不可避免的异常),这将更有帮助。尽管如果没有强制的异常声明,它可能不会那么可靠,但是大多数异常仍然会在文档中声明,而且IDE警告的可靠性也不是那么重要。
下面是一个反对检查异常的论点(来自joelonsoftware.com):
The reasoning is that I consider exceptions to be no better than
"goto's", considered harmful since the 1960s, in that they create an
abrupt jump from one point of code to another. In fact they are
significantly worse than goto's:
-
They are invisible in the source code. Looking at a block of code,
including functions which may or may not throw exceptions, there is no
way to see which exceptions might be thrown and from where. This means
that even careful code inspection doesn't reveal potential bugs.
-
They create too many possible exit points for a function. To write correct
code, you really have to think about every possible code path through
your function. Every time you call a function that can raise an
exception and don't catch it on the spot, you create opportunities for
surprise bugs caused by functions that terminated abruptly, leaving
data in an inconsistent state, or other code paths that you didn't
think about.
- +1不过,您可能想在回答中总结一下这个论点?它们就像看不见的哥特人和你日常生活的早期出口,分散在整个程序中。
- 一般来说,这更像是一个反对例外的论据。
- 你真的读过这篇文章吗?!首先,他一般谈论异常,其次,"它们在源代码中不可见"一节专门适用于未检查的异常。这是检查异常的全部要点…这样你就知道什么代码把什么扔到哪里了
- 反对检查异常的部分理由是,实际上您不知道在哪里抛出了什么。是的,您知道一个方法可能会抛出其声明的检查异常(尽管有许多恼人的情况,其中声明了一个异常,即使您知道特定的实现永远不会抛出该异常)。而且,它可以抛出任何未经检查的异常。关注前者往往会使开发人员忽视后者。
- 人们会不会放弃"这是一个伪装的哥特人"!如果语句是goto的一种形式,那么循环就是goto的一种形式。哥特人的问题是他们太强大,不够具体。你可以在任何地方使用goto,这使得你很难知道何时停止。当您有一个"goto"结构(如异常、break语句、continue等)时,您可以确切地知道它的用途。
- @伊娃,他们不一样。使用goto语句,可以看到goto关键字。通过循环,您可以看到右大括号或break或continue关键字。它们都跳到当前方法中的一个点上。但您不能总是看到throw,因为它通常不在当前方法中,而是在它调用的另一个方法中(可能是间接调用的)。
- @finnw函数本身就是goto的一种形式。您通常不知道正在调用的函数是什么函数。如果您编程时没有函数,那么您就不会遇到不可见异常的问题。这意味着问题并不是特别与异常相关的,也不是针对一般异常的有效论据。你可以说错误代码更快,你可以说monads更干净,但是goto的论点是愚蠢的。
- @实际上,错误代码的速度较慢,因为在每次可能失败的操作(包括所有级别的委托)之后,它们都需要重复检查,而异常可以跳过这些调用链,直接跳到处理程序,而普通的代码流没有条件的混乱。在异常记录堆栈跟踪的语言中,它们通常较慢,但只有在创建异常时才会如此。
- @霍格尔…或者在没有stacktrace的情况下创建它们?
- @Eugene好吧,当您禁用堆栈跟踪时,异常确实会变得非常快,但是我仍然建议使用一种编码方式,在这种方式下,异常会保留异常,因此,它们的创建成本无论如何都是不相关的。
- @霍尔格是对的,但我的理解是上下文是goto和exceptions,如果是,为什么我需要堆栈跟踪?
- @尤金不完全是。我只是指"错误代码与异常"的性能比较。因此,在这两种情况下,我们都有一个正常的代码流,在这种情况下,使用异常的速度更快,而错误的情况下,在记录堆栈跟踪时,异常的速度较慢,这不应该是个问题。如果您想将异常作为某种goto来滥用,禁用跟踪将有助于性能;我曾经通过禁用该特定异常的堆栈跟踪来修复了此类代码,但后来我重写了整个代码,以避免滥用异常,这对我来说甚至更好…
没有人提到的一件重要事情是它如何干扰接口和lambda表达式。
假设您定义了一个MyAppException extends Exception。它是由应用程序引发的所有异常继承的顶级异常。每个方法都声明throws MyAppException,这有点多余,但可以管理。异常处理程序记录异常并以某种方式通知用户。
在您想要实现一些不是您的接口之前,一切看起来都正常。显然,它不声明抛出MyApException的意图,因此编译器不允许您从那里抛出异常。
但是,如果您的异常扩展了RuntimeException,那么接口就不会有问题。如果愿意的话,您可以在javadoc中自愿地提到这个例外。但除此之外,它只是无声地冒泡通过任何东西,被捕获在您的异常处理层。
良好的证据表明,不需要检查异常:
很多为Java工作的框架。像Spring将JDBC异常包装为未检查的异常,将消息抛出日志
Java之后的许多语言,甚至在Java平台上都是顶级的——它们不使用它们。
检查异常,这是关于客户端如何使用引发异常的代码的友好预测。但是编写这段代码的开发人员永远不会知道代码客户机所使用的系统和业务。作为一个例子,强制抛出检查异常的交互方法。系统上有100个实现,50个甚至90个实现都不会引发此异常,但是如果用户引用该接口,客户机仍然必须捕获此异常。这50或90个实现倾向于在自身内部处理这些异常,将异常放到日志中(这对它们来说是很好的行为)。我们该怎么办?我最好有一些后台逻辑来完成所有的工作——向日志发送消息。如果我作为代码的客户,觉得我需要处理这个异常,我会做的。我可能会忘记它,对吧-但如果我使用TDD,我的所有步骤都会被覆盖,我知道我想要什么。
另一个例子,当我在Java中使用I/O时,它强迫我检查所有异常,如果文件不存在?我该怎么办?如果它不存在,系统将不会进入下一步。此方法的客户端无法从该文件中获取预期的内容-他可以处理运行时异常,否则我应该首先检查检查异常,将消息记录到日志中,然后从该方法中抛出异常。不…不-我最好用runtimeeception自动完成,这会自动完成。手动处理没有任何意义-我很高兴在日志中看到错误消息(AOP可以帮助解决这个问题)。修复Java的东西。如果,最终,我认为系统应该向最终用户显示弹出消息-我将显示它,而不是一个问题。
我很高兴,如果Java能为我提供一个选择,当使用核心LIBS时,像I/O一样,它提供了两个相同类的副本,一个是RunTimeEclipse包装的。然后我们可以比较人们会使用什么。但是现在,许多人最好在Java或其他语言上寻找一些框架。就像斯卡拉,JRuby什么的。很多人只是相信太阳是对的。
- 不是有两个版本的类,而是应该有一种简洁的方法来指定一个代码块所做的方法调用中没有一个不会抛出某些类型的异常,并且任何此类异常都应该通过某些指定的方法包装并重新引发(默认情况下,创建一个具有适当内部异常的新RuntimeException。离子)。不幸的是,让外部方法throws作为内部方法的异常要比让它包装内部方法的异常简洁得多,后者的操作过程往往是正确的。
我认为这是一个很好的问题,完全没有争议。我认为第三方库应该(一般)抛出未经检查的异常。这意味着您可以隔离您对库的依赖(即,您不必重新抛出它们的异常或抛出Exception—通常是错误的做法)。春天的刀层就是一个很好的例子。
另一方面,核心JavaAPI的异常通常应检查是否可以处理。以FileNotFoundException或(我最喜欢的)InterruptedException为例。这些条件几乎总是要特别处理(即,你对InterruptedException的反应与你对IllegalArgumentException的反应不同)。检查异常的事实迫使开发人员考虑一个条件是否可以处理。(也就是说,我很少看到InterruptedException处理得当!)
还有一件事——RuntimeException并不总是"开发人员出了问题"。当你试图用valueOf创建一个enum并且没有该名称的enum时,就会抛出一个非法的参数异常。这不一定是开发人员的错误!
- 是的,这是开发人员的错误。他们显然没有使用正确的名字,所以他们必须回去修改他们的代码。
- @AxiomaticExus没有一个理智的开发人员使用enum成员名,这仅仅是因为他们使用enum对象。所以一个错误的名字只能来自外部,不管是导入文件还是其他什么。处理这类名字的一种可能方法是调用MyEnum#valueOf并抓住国际原子能机构。另一种方法是使用预先填充的Map,但这些是实现细节。
- @maaartinus在某些情况下,使用枚举成员名称时不使用来自外部的字符串。例如,当您希望动态循环所有成员以对每个成员执行某些操作时。此外,字符串是否来自外部无关紧要。开发人员拥有他们所需要的所有信息,以知道在传递x字符串到"myenum valueof"之前是否会导致错误。将x字符串传递给"myenum valueof",不管怎样,当它会导致错误时,显然是开发人员的错误。
我们已经看到一些关于C首席建筑师的参考资料。
这里有一个Java人关于使用检查异常的替代观点。他承认其他人提到的许多否定:有效例外
- Java中检查异常的问题源于一个更深层次的问题,即太多的信息被封装在异常的类型中,而不是封装在实例的属性中。如果"checked"是throw/catch站点的一个属性,并且可以声明性地指定逸出代码块的已检查异常是否应保持为已检查异常,或者被任何封闭块视为未检查异常,则检查异常将非常有用;同样,catch块应能够指定他们只需要检查异常。
- 假设一个字典查找例程被指定为在试图访问一个不存在的键时抛出某种特定类型的异常。客户端代码捕获这样的异常可能是合理的。但是,如果查找例程使用的某个方法碰巧以查找例程不期望的方式引发了相同类型的异常,则客户端代码可能不应该捕获它。检查ness是异常实例、抛出站点和捕获站点的一个属性,可以避免此类问题。客户端将捕获该类型的"已检查"异常,从而避开意外的异常。
尽管读了整页,我还是找不到一个合理的理由来反对检查过的异常。大多数人讨论的是API设计不佳,无论是在Java类还是在自己的类中。
唯一可能让此功能恼人的场景是原型。这可以通过向语言中添加一些机制来解决(例如,一些@supresscheckedexceptions注释)。但是对于常规编程,我认为检查异常是一件好事。
- 最佳实践"早抛,晚抓"与检查异常坚持立即处理不兼容。它还阻止了FP功能编程方法。参见:识字java.com/exceptions/&hellip;
- 没有坚持立即处理,只有处理。您可以声明您的方法抛出了一些异常并尽可能晚地捕获它们。你也可以尽早扔。
- 通过调用树进行指数扩展实际上是对立即处理的坚持。如果仅适用于可预测和潜在可恢复的突发事件,这可能是值得的,但"检查"行为错误地扩大到了不可预测和不可恢复故障的广泛范围。文件open'或'connect jdbc'要求检查是合理的——大多数其他IOexception、sqlexception、remoteexception不是这样。这是Java库设计中的一个主要错误。请参阅我的答案以及有关异常处理的基本入门。
- @托马斯,我读过你的链接和答案,但我还是不同意。检查的异常一般来说都不错,尽管我同意API中的某些特定异常应该被取消检查。
- @托马斯和"早抛,晚抓"的原则是很有争议的。提前投球是可以的,但你不能总是误投。这要看情况而定。例如,如果我正在编码一个与WS接口的类,我可能会碰到,或者说,IOException、ParsingException、RemoteException、PermissionException等等。在这里,我们可以及早捕获并在这个层中处理它们,或者返回null/false/-1/无论什么,或者将它们封装在自定义wsexception中(当然选中了,否则客户机类可能会忘记处理它们)。
- "延迟捕获"是基于可以隔离故障的级别——大多数情况下,这些级别是业务/请求或出站连接/请求级别。简单和正确。
- 返回null/false/-1是不正确的做法,因为它错误地代表了客户的成功!这是一个严格的"否",因为它允许执行继续使用不完整/无效/不正确的数据,以便稍后失败(错误)或提交到数据库(更糟)。如果业务逻辑的某些部分是真正可选的,而您没有声明,那么Try/Catch允许这些部分继续发出警告。无效值和在应用程序周围传播坏数据既不好也不必要。
- 最佳实践异常处理实际上基于如何正确地处理异常/错误(记录、报告,有时恢复)。这是一门科学,不是一门艺术……获得100%的最佳和正确实际上是简单和容易的——只要我们不被(错误的库设计)推到"尽早处理"。如我们所见,这主要是鼓励错误的做法。
- @先生,如果你在某个接近最高水平的地方,你永远不会忘记处理一个WSException或任何其他东西。您可能需要或可能不需要特殊处理,但基本原则是处理所有事情。对于特殊处理,您可以使用try {...} catch (WSException) {...} catch (Exception e) {...},但只有当您可以对它做一些特殊的事情时才需要它(您可能想重试,但这也适用于TransactionLockTimeExceeded)。
我读过很多关于异常处理的文章,即使(大多数时候)我不能真的说我对检查过的异常的存在感到高兴或难过,这是我的看法:在低级代码(IO、网络、OS等)中检查过异常,在高级API/应用程序级别中检查过异常。
即使在它们之间划一条线并不容易,但我发现在同一屋檐下集成多个API/库而不总是包装许多检查过的异常确实很烦人/困难,但另一方面,有时强制捕获一些异常并提供不同的异常会更有用/更好在当前上下文中有意义。
我正在处理的项目需要很多库,并将它们集成在同一个API(完全基于未检查异常的API)下。这个框架提供了一个高层API,它在开始时充满了检查异常,并且只有几个未检查异常(初始化异常、配置异常等),我必须萨伊不是很友好。大多数情况下,您必须捕获或重新抛出您不知道如何处理的异常,或者您甚至不在乎(不要与您混淆,应忽略异常),特别是在客户端,单击一次可以抛出10个可能的(选中的)异常。
当前版本(第三个版本)只使用未选中的异常,它有一个全局异常处理程序,负责处理任何未捕获的异常。API提供了一种注册异常处理程序的方法,该方法将决定是否将异常视为错误(大多数情况下是这样),这意味着日志和通知某人,或者它可能意味着其他事情,如此异常、异常异常,这意味着中断当前执行线程,不记录任何错误,因为不需要记录任何错误。T to。当然,为了计算出所有自定义线程必须使用try…catch(all)处理run()方法。
public void run()。{
1 2 3 4 5
| try {
... do something ...
} catch (Throwable throwable ) {
ApplicationContext. getExceptionService(). handleException("Handle this exception", throwable );
} |
}
如果您使用WorkerService来计划作业(可运行、可调用、Worker),则不需要这样做,因为它可以为您处理所有事情。
当然,这只是我的观点,可能不是正确的观点,但对我来说,这似乎是一个很好的方法。我会看到在我发布这个项目之后,如果我认为它对我有好处,对其他人也有好处…:)
在我看来,检查异常是一个非常好的概念。不幸的是,大多数一起工作的程序员,我们有另一个观点,这样项目就有很多错误的使用异常处理。我已经看到大多数程序员创建了一个(只有一个)异常类,一个RuntimeException的子类。它包含一条消息,有时是一个多语言键。我没有机会反驳这一点。我的印象是,当我向他们解释反模式是什么,方法的契约是什么时,我会和一堵墙交谈……我有点失望。
但是,今天已经很明显了,对所有东西都有一个通用的运行时异常的概念是反模式的。他们使用它来检查用户输入。抛出异常以便用户对话可以从中生成错误消息。但并非每个方法调用方都是对话!通过引发运行时异常,该方法的约定已更改但未声明,因为它不是已检查的异常。
希望他们今天学到了一些东西,并且会在另一个地方进行检查(这是有用和必要的)。只使用检查的异常不能解决问题,但是检查的异常会向程序员发出信号,表明他实现了错误的东西。