在Java中,检查异常有什么用处?[关闭]


14

多年来,Java的受检查异常受到了一些负面报道。一个明显的迹象是,它实际上是世界上唯一拥有它们的语言(甚至不是其他JVM语言,例如Groovy和Scala)。诸如Spring和Hibernate之类的著名Java库也不使用它们。

我个人已经发现了它们的一种用途(层之间的业务逻辑中),但除此之外,我还是非常反对检查的异常。

还有其他我没有意识到的用途吗?


所有Java核心库都非常广泛地使用检查的异常。
乔纳斯·普拉卡

3
@Joonas我知道,这很痛苦!
布拉德·库皮

Answers:


22

首先,像其他任何编程范例一样,您需要正确地进行操作才能使其正常工作。

我而言,检查异常的优点是Java运行时库的作者已经为我决定了可以合理预期在调用点处可以解决哪些常见问题(与顶级catch-print-死块),并尽早考虑如何处理这些问题。

我喜欢检查异常,因为它们使我不得不尽早考虑错误恢复,从而使我的代码更健壮。

确切地说,对我来说,这使我的代码更加健壮,因为它迫使我在处理过程的早期就考虑奇怪的极端情况,而不是说基于:生产中的错误,然后必须重新处理代码以进行处理。在进行维护时,将错误处理添加到现有代码中可能不是一件容易的事-因此非常昂贵,而不是一开始就做。

丢失的文件可能是致命的,应该导致程序崩溃。但是,可以使用

} catch (FileNotFoundException e) {
  throw new RuntimeException("Important file not present", e);
}

这也显示出非常重要的副作用。如果您包装了一个异常,则可以在堆栈跟踪中添加一个解释!这种功能非常强大,因为您可以添加有关例如丢失文件名或传递给此方法的参数的信息或其他诊断信息,并且该信息直接出现在堆栈跟踪中,而这通常只是您要做的事情当程序崩溃时获取。

人们可能会说“我们可以在调试器中运行此程序以进行重现”,但是我发现很多时候以后无法重现生产错误,并且除非在非常讨厌的情况下,否则您就不能在生产中运行调试器,这在本质上会危及您的工作。

堆栈跟踪中的信息越多越好。受检查的异常有助于我尽早获得该信息。


编辑:这也适用于图书馆设计师。我每天使用的一个库包含许多检查过的异常,这些异常本可以设计得更好,从而减少了使用时的乏味。


+1。Spring的设计师在将许多Hibernate异常转换为未经检查的异常方面做得很好。
直到2010年

仅供参考:Frank Sommers在此主题中提到容错软件通常依赖于完全相同的机制。也许您可以将答案细分为可恢复的情况和致命的情况。
rwong

@rwong,能否请您详细解释一下您的想法?

11

您有两个很好的答案,它们解释了实践中已检查的异常。(两者均为+1。)但是,从理论上来说,检查它们的意图也是值得的,因为意图实际上是值得的。

检查异常实际上是为了使语言更安全。考虑一个简单的方法,例如整数乘法。您可能会认为此方法的结果类型将是整数,但是严格来说,结果可能是整数或溢出异常。仅将整数结果视为方法的返回类型不能表示函数的完整范围。

从这个角度来看,并不是说被检查的异常没有被其他语言发现是不正确的。他们只是没有采用Java使用的形式。在Haskell应用程序中,通常使用代数数据类型来区分功能的成功完成与失败。尽管这本身并不是一个例外,但其意图与已检查的例外非常相似。它是一个旨在强制API使用者在不成功的情况下成功处理这两个问题的API,例如:

data Foo a =
     Success a
   | DidNotWorkBecauseOfA
   | DidNotWorkBecauseOfB

这告诉程序员该函数除成功外还有两个其他可能的结果。


+1是关于类型安全性的已检查异常。我以前从未听过这个主意,但这很有道理。
Ixrec 2015年

11

IMO,checked异常是一个有缺陷的实施什么可能是一个相当不错的主意(下完全不同的情况-但真的不是在目前的情况下,即使是一个好主意)。

好的主意是,让编译器验证是否可以捕获程序可能引发的所有异常会很好。有缺陷的实现是这样做的,Java迫使您直接在任何类调用声明为引发已检查异常的任何类中处理异常。异常处理的最基本的思想(和优点)之一是,在发生错误的情况下,它允许与特定错误无关的中间代码层完全忽略其本身的存在。受检查的异常(用Java实现)不允许这样做,因此破坏了从异常处理开始的主要优势(主要优势)。

从理论上讲,通过仅在整个程序范围内强制执行检查的异常,它们可以使检查异常有用-即,在“构建”程序的同时,构建程序中所有控制路径的树。树中的各个节点将使用1)可以生成的异常和2)可以捕获的异常进行注释。为了确保程序正确,您需要遍历树,验证在树中任何级别抛出的每个异常都在树中更高的级别被捕获。

他们之所以没有这样做(无论如何,我想是因为这样)非常困难-实际上,我怀疑有人编写了一个构建系统,该系统可以实现简单的操作(对“玩具”进行边框处理) “)语言/系统。对于Java,诸如动态类加载之类的事情使它比许多其他情况更加困难。更糟糕的是,(至少与正常情况不同,)Java(至少在正常情况下)不是静态编译/链接到可执行文件的。这意味着没有在系统中真正具有全球意识,甚至尝试做这样的执法,直到最终用户真正开始加载程序来运行它。

由于以下几个原因,在那时执行执法是不切实际的。首先,即使充其量也可以延长启动时间,这已经是对Java的最大,最常见的投诉之一。其次,要做很多事情,您确实需要尽早发现问题,以便程序员加以解决。当用户加载的程序,这是来不及做多好-你在这一点上唯一的选择是忽视的问题,或拒绝加载/在所有运行程序,上关的机会,有可能是一个例外没有被抓住。

实际上,检查异常比没有用要糟,但是检查异常的替代设计甚至更糟。虽然checked异常的基本思想似乎是一个不错的一个,它真的不是很好,恰好是一个特别不合身的Java。对于像C ++这样的东西,通常可以将其编译/链接到一个完整的可执行文件中,而没有像反射/动态类加载这样的东西,您将有更多的机会(尽管坦率地说,即使正确执行它们,也不会造成混乱) Java中的当前原因可能仍然超出当前的技术水平)。


关于必须直接处理异常的第一个初始声明已经是错误的;您可以简单地添加一个throws语句,并且throws语句可以是父类型。此外,如果您真的不认为必须要处理它,则只需将其转换为RuntimeException即可。IDE通常可以帮助您完成这两项任务。
Maarten Bodewes 2014年

2
@owlstead:谢谢-我可能应该对那里的措辞更加谨慎。关键不是要您实际上必须捕获异常,而是因为每个中间代码级都需要意识到可能流经它的每个异常。至于其余的事情:拥有一个可以使您轻松地编写代码的工具不会改变它使代码混乱的事实。
杰里·科芬

受检查的异常用于意外事件 -除失败之外,与成功不同的可预测且可恢复的结果-与故障不同-不可预测的低级/或系统问题。正确考虑了FileNotFound或打开JDBC连接时出错。不幸的是,Java库设计人员犯了一个完全错误,并将所有其他IO和SQL异常也分配给了“ checked”类。尽管这些几乎完全无法恢复。 literatejava.com/exceptions/...
托马斯W ^

@owlstead:如果在中间调用代码期望的情况下抛出了检查异常,则仅在调用堆栈的中间层中冒泡。恕我直言,如果呼叫者不得不抓住它们或表明是否期望它们,检查异常将是一件好事。意外的异常应自动包装在UnexpectedException从派生的类型中RuntimeException。请注意,“抛出”和“不期望”并不矛盾。如果foo抛出BozException并发出呼叫bar,那并不意味着它期望bar抛出BozException
超级猫2014年

10

它们确实非常适合烦人的程序员和鼓励吞咽异常。异常的一半是让您拥有处理错误的默认方法,而不必考虑显式传播无法处理的错误条件。(理智的默认设置是,如果您从不在代码中的任何位置处理错误,则可以有效地断言该错误不会发生,并且如果您错了,则会留下理智的退出和堆栈跟踪。)检查异常失败这个。他们感觉很像必须在C中显式传播错误。


4
轻松解决:throws FileNotFoundException。硬性修正:catch (FileNotFoundException e) { throw RuntimeException(e); }
Thomas Eding

5

我认为可以说检查异常是一个失败的实验。他们出了错的地方是他们打破了层次:

  • 模块A可能构建在模块B的顶部,其中
  • 模块B可能构建在模块C的顶部。
  • 模块C可能知道如何识别错误,并且
  • 模块A可能知道如何处理该错误。

关注点之间很好的分离被打破了,因为现在必须将错误的知识原本仅限于A和C散布在B上。

Wikipedia上有一个关于检查异常的很好的讨论

布鲁斯·埃克尔(Bruce Eckel)也有一篇不错的文章。这是一个报价:

堆积在程序员身上的随机规则越多,与解决当前问题无关的规则,程序员产生的速度就越慢。这似乎不是一个线性因素,而是一个指数因素

我相信对于C ++,如果所有方法都有throws规范子句,那么您可以进行一些优化。但是我不相信Java会具有这些优势,因为RuntimeExceptions未经检查。无论如何,现代的JIT应该能够优化代码。

AspectJ的一个不错的功能是异常软化:您可以将已检查的异常转换为未检查的异常,专门用于增强分层。

也许这是具有讽刺意味的Java通过这一切的麻烦,当它可能检查的去null替代,这可能是更有价值的


哈哈,从未考虑过将空值检查作为他们没有做出的决定……但是我终于明白,这不是在Objective-C中工作之后的先天问题,它允许空值调用方法。
Dan Rosenstark 2010年

3
空检查会带来更大的痛苦-您总是必须进行检查-而且它不能告诉您原因,众所周知的异常会导致这种情况。

5

我喜欢例外。

但是,我经常被他们的误用所困扰。

  1. 不要将它们用于流处理。您不希望代码尝试执行某项操作并使用异常作为触发器来执行其他操作,因为异常是需要创建并使用内存等的对象,等等,只是因为您不必费心编写一些打电话之前先检查一下if()的种类。那只是懒惰,给光荣的Exception一个坏名字。

  2. 不要吞下它们。曾经 在任何情况下。绝对最低限度,您在日志文件中粘贴一些内容以指示发生了异常。试图通过吞噬异常的代码来跟踪自己的方式是一场噩梦。再次,诅咒您使异常强大的异常声名狼藉的异常破坏者!

  3. 戴上OO帽子,不要对待异常。创建具有有意义的层次结构的自己的Exception对象。并行处理您的业务逻辑和异常。我曾经发现,如果我是从系统的新领域开始的,那么我会发现需要在编码的半小时内将Exception子类化,所以如今,我开始编写Exception类。

  4. 如果您要编写“框架式”的代码,请确保编写了所有初始接口,以在适当的地方抛出自己的新异常...,并确保您的编码人员在其代码编写位置附近有一些示例代码引发IOException,但是他们正在写入的接口希望引发'ReportingApplicationException'。

一旦摸索了如何使Exceptions正常工作,您将永远不会回头。


2
+1为良好的指示。另外,在适当的地方,使用initCause或初始化引起异常的异常。示例:您有一个I / O实用程序,只有在写入失败时才需要一个异常。但是,有多种原因导致写操作可能失败(无法访问,写错误等)。将自定义异常与原因1一起抛出,以免原始问题被吞没。
Michael K

3
我不想太粗鲁,但这与CHECKED异常有什么关系?:)
palto 2012年

可以重写以编写您自己的检查异常的层次结构。
Maarten Bodewes 2014年

4

受检查的异常也在ADA中。

(警告,此帖子包含您可能会遇到的坚定信念。)

程序员不喜欢他们并抱怨,也不编写吞咽异常的代码。

存在已检查的异常是因为事情不仅会失败,您还可以进行故障模式/效果分析并事先确定。

文件读取可能会失败。RPC调用可能会失败。网络IO可能会失败。解析时数据的格式可能不正确。

代码的“快乐之路”很容易。

我认识一个大学里的人,他可以编写出色的“幸福之路”代码。没有一个极端案例能奏效。如今,他为一家开源公司使用Python。纳夫说。

如果您不想处理检查的异常,那么您真正要说的是

While I'm writing this code, I don't want to consider obvious failure modes.

The User will just have to like the program crashing or doing weird things.

But that's okay with me because 

I'm so much more important than the people who will have to use the software

in the real, messy, error-prone world.

After all, I write the code once, you use it all day long.

因此,程序员不会喜欢检查异常,因为这意味着更多工作。

当然,其他人可能希望完成这项工作。

即使文件服务器出现故障/ USB记忆棒死掉,他们也可能希望找到正确的答案。

在编程社区中,一个奇怪的信念是,当您的工作是编写软件时,您应该使用一种使生活更轻松,更享受的编程语言。您的工作是在解决某人的问题,而不是让您从事程序化的爵士即兴演奏。

如果您是一名业余程序员(不是为了钱而编程),请随时使用C#或其他语言进行编程,没有经过检查的异常。哎呀,剪掉中间人和徽标中的程序。您可以使用乌龟在地板上绘制漂亮的图案。


6
好家伙,你到了那里。
亚当李尔

2
+1“没有一个极端的案例能奏效。如今,他为一家开源公司使用Python。Nuff说。” 经典
NimChimpsky 2010年

1
@palto:Java 迫使您考虑不容易的路径。那是最大的不同。对于C#,您可能会说:“记录了异常”,然后洗手。但是程序员很容易出错。他们忘记了事情。编写代码时,我想100%提醒编译器可以通知我的任何漏洞。
Thomas Eding

2
@ThomasEding Java强制我处理调用方法的异常。我实际上很少想在调用代码中对此做些什么,而且通常我想将异常冒泡到应用程序的错误处理层中,这种情况很少见。然后,我必须通过将异常包装在运行时异常中来补救它,或者必须创建自己的异常。懒惰的程序员还有第三种选择:因为我不在乎,请忽略它。这样一来,没人知道错误发生了,并且该软件确实存在奇怪的错误。没有未经检查的异常不会发生第三个问题。
palto 2012年

3
@ThomasEding这样做将更改上面所有内容的接口,从而不必要地向应用程序的每一层揭示实现细节。仅当您要从中恢复异常并且在界面中有意义时,您才可以这样做。我不想将IOException添加到我的getPeople方法中,因为可能缺少某些配置文件。只是没有意义。
palto 2012年

4

受检查的异常应该鼓励人们处理源头附近的错误。离源越远,在运行过程中终止的功能越多,并且系统进入不一致状态的可能性越大。此外,更有可能是您偶然捕获了错误的异常。

可悲的是,人们倾向于忽略异常或增加不断增加的抛出规范。这两个都错过了应该鼓励检查异常的意义。它们就像将过程代码转换为使用许多Getter和Setter函数(据说使其成为OO)一样。

本质上,检查异常正在尝试证明代码中的一定程度的正确性。它与静态类型几乎相同,它试图在代码运行之前证明某些事情是正确的。问题在于,所有这些额外的证明都需要付出努力,值得付出努力吗?


2
“忽略异常”通常是正确的-很多时候,对异常的最佳响应是使应用程序崩溃。对于这种观点的理论基础,请阅读Bertrand Meyer的“ 面向对象的软件构造”。他表明,如果正确使用了异常(针对违反合同的行为),则基本上有两个正确的响应:(1)使应用程序崩溃或(2)解决导致异常的问题并重新启动该过程。阅读Meyer了解详细信息;很难在评论中进行总结。
Craig Stuntz 2010年

1
@Craig Stuntz,我无视异常是吞下它们。
温斯顿·埃韦特2010年

啊,不一样。我同意你不应该吞下他们。
Craig Stuntz 2010年

“忽略异常通常是正确的-很多时候,对异常的最佳响应是使应用程序崩溃。”:这实际上取决于应用程序。如果您在Web应用程序中有未捕获的异常,则可能发生的最坏情况是您的客户将报告在网页上看到一条错误消息,并且您可以在几分钟内部署错误修复程序。如果飞机降落时有异常情况,则最好处理并尝试恢复。
Giorgio

@乔治,非常真实。尽管我想知道是否在飞机或类似飞机上,恢复的最佳方法是重启系统。
Winston Ewert 2014年
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.