Java异常层次结构,有什么作用?

4

我发现自己正忙于重构我的宠物项目,并消除与异常抛出声明相关的所有噪音。基本上,违反条件的所有内容都是断言违规,或者在Java中正式地称为AssertionError,可以慷慨地省略方法签名。我的问题是:有什么必要建立异常层次结构?我的经验是每个异常都是独一无二的,没有正式的标准来确定一个异常集合是否是另一个异常集合的子集。即使是已检查和未检查的异常之间的区别也很模糊,例如,当一个懒惰(或急躁)的程序员可以轻松地将其包装成RuntimeException并重新抛出时,为什么我要坚持客户端代码捕获异常呢?


5
懒惰和/或急躁的程序员可能会破坏任何语言机制,包括“if”语句。 - JUST MY correct OPINION
@Tegiri Nenashi:我不确定你能在这里找到诚实的答案。这里有很多人喝了Java的“酷-aid”,认为Java拥有的一切都是完美的。但也有一些人不这么认为。需要注意的是,检查异常是一个非常有争议的话题:许多语言甚至没有这个概念,但仍然可以很好地运行。甚至有一些非常强大和专业的Java框架,比如Spring,建议最小化使用检查异常。检查异常的问题在于它们基本上就像GOTO语句。许多语言没有GOTO语句也能很好地运行。 - SyntaxT3rr0r
@Tegiri Nenashi:每次我看到有人创建自己的异常,我就忍不住想:“这在OOA/OOD级别上不存在”。换句话说:自定义(并检查)异常是Java的特殊用法。 - SyntaxT3rr0r
3
@Webinator: 如果我能够点踩评论,那我一定会这么做。假设你不诚实让我想知道为什么你还要来这个网站。毕竟,没有人是诚实的,对吧?我想我会心理记住不信任你写的任何答案,因为通常我们会指责“另一个人”拥有我们自己思想中所显示的缺陷。 - JUST MY correct OPINION
@仅是我的正确观点:那么如何解决我的问题呢?祝你愉快地创建自己的异常并生活在Java特有的梦境中 ;) - SyntaxT3rr0r
1
你在第一次回复的第一句话中已经非常明确地表达了你的观点:“我不确定你会在这里找到诚实的答案。”除此之外的任何言论都是毫无意义的废话。 - JUST MY correct OPINION
3个回答

11
理论上,Java的异常层次结构有一定的道理:
Throwable*
 -> Error (OutOfMemoryError, etc.)
 -> Exception (IOException, SQLException, etc.)
     -> RuntimeException (IndexOutOfBoundsException, NullPointer, etc.)

这些异常的理论背后实际上是有一定道理的。(不幸的是,由于积累的垃圾代码,实际实现留下了一些问题。)

Error-派生的Throwable对象是严重的错误,程序不应该能够从中恢复。(换句话说,通常情况下你不会捕捉到这些错误。)出现其中之一表示整个系统存在严重问题。例如,当您耗尽内存时,这表示某处存在严重的问题,因为理论上来说,GC已经拼命地为您释放空间。捕获这种错误是毫无意义的。

Exception-派生的Throwable对象是程序在正常操作期间可以合理预期遇到的所有错误;例如网络错误、文件系统错误等等。事实上,除了那些衍生自RuntimeException的对象外,程序被要求明确处理这些错误—它们被称为"受检异常"。(当然,糟糕的程序员会通过将其替换为占位符来“处理”这些异常,但这是一个程序员的问题,而不是系统问题。)

RuntimeException-派生的Throwable对象略有不同。它们是不一定可以预期但程序在发生时可以合理恢复的错误。因此它们没有被检查(程序没有义务处理这些异常),但如果有合理的方法来处理该情况,它们可能会被处理。通常,这些异常代表某种编程错误(而不是前面的类别中在正常操作中发生的预期错误)。

那么这个层次结构是否有意义呢?在某种程度上似乎是有道理的。Error由系统抛出,表示可能会使您的程序崩溃的重大故障。RuntimeException在适当使用时,由系统库(或偶尔是您自己的程序)抛出,并且通常意味着某个人在某个地方搞砸了,但没关系,因为您可能能够从中恢复。其他Exception对象是预期的错误,实际上是您对象的声明接口的一部分。

但是...

最后一个条目就是问题所在。明确的受检异常毫无疑问是下半身疼痛的严重问题。以这种方式使每个方法在整个调用链中都要处理异常—即使只是重新包装并传递它!—代码被杂乱的异常处理模板填塞到了一起,以至于它与返回状态码并在每个方法调用后处理它们的过程几乎没有什么区别。

如果Java是一种更智能的编程语言,那么受检异常将在编译/链接时检查它们是否在整个系统中得到了正确处理。不幸的是,Java的整个架构并不允许进行这种整个程序分析的层面,结果是,在我看来(但许多人也共享这种看法),它实际上融合了异常处理和错误返回两个世界中最糟糕的方面:您会得到大部分显式错误返回的脚手架,但您也会得到类似于COME FROM的异常行为。


3
我的经验是每个异常都是独一无二的,没有正式的标准来确定一个异常集合是否是另一个的子集。甚至检查和未检查异常之间的区别也很模糊,为什么要强制客户端代码捕获异常,当一个懒惰(或者急躁)的程序员可以轻松地将其包装成 RuntimeException 并将其重新抛出给我呢?
这一切都是正确的。
就像 Joel Spolsky 曾经说过的:“设计就是选择的艺术”。因此,设计异常层次结构涉及到做出选择。有时候它们效果很好,有时候则不然。
我看到 [拥有异常层次结构] 的一个优点是通过捕获超类型异常,你可以使用单个 catch 子句捕获一整个异常类型集合。用户总是想要这样吗?当然不是。在那些(且仅在那些)它不是他想要的情况下,用户能够摆脱它吗?当然可以。没有超类型异常的替代方案更好吗?在我看来肯定不是。看看 IOException 层次结构,想象一下在每个可能出现的地方都需要为每个子类编写一个 catch 子句...
即使有所有争议性的缺点,到目前为止,我仍然相信 Java 的异常处理机制胜过我见过的任何其他机制。

0

我认为这种层次结构并不是坏事,只是不太有用。99%的情况下,异常信号表明发生了非常严重的问题,而我的选择很少。我们基本上已经死了。唯一的选择是选择适当的错误消息来显示给用户。

我是那些懒惰的程序员之一。如果我从代码深处调用'processFile()'方法,它抛出异常,我可能会制定一个“无法处理您的文件”的消息,并重新抛出RuntimeException。我们不能恢复。我们是一个过去式。在调用堆栈中每个方法上添加检查异常会使代码变得混乱,没有任何好处。

不可避免地,我会编写像这样的代码:

try {
    processAFile();
} catch (Exception e) { // Just catch them all.
    logger.error(e);
    logger.error("log any important information here.");
    throw new RuntimeException("We were unable to process your file.");
}

现在RuntimeException一路嘎吱作响到主方法,并得到负责处理。

在代码的顶部,我捕获所有异常,根据需要记录日志,通常回滚事务,并执行必要的操作以显示友好的错误消息。

我喜欢已检查和未检查异常之间的区别,尽管我想不出为什么。IntelliJ将自动填充几个异常捕获块。我想:“嗯,这很有趣”,然后用一个catch(Exception e)替换它们,因为恢复总是相同的。在上面的示例中,我必须捕获已检查和未检查的异常,然后我就死了,错误消息是相同的,所以谁在乎。为什么要抛出已检查的异常。死就是死。

我唯一能想到需要处理特定异常的情况是当我调用一个不正确地抛出异常而不是返回错误条件的方法时。


99%的时间,异常信号意味着发生了非常糟糕的事情,我的选择很少。我们基本上已经死了。我恐怕不能同意这一点。Error抛出的是你所描述的内容。RuntimeException抛出的通常是你所描述的内容(尽管在某些情况下可以解决)。但是,如果你正确使用层次结构(并且如果你能够接受检查异常),Exception抛出的是明确用于信号系统可以从中恢复的事情。它们被认为是 API 的“正常”(但异常?)部分。 - JUST MY correct OPINION
它们被认为是 API 的“正常”(但卓越的?)部分。就是这样。这就是为什么我喜欢看到它们。但是通常我会忽略我调用的方法抛出的具体异常。 :-/ - Tony Ennis
嗯,是的。我丝毫不认为检查异常是正确的。我认为它们是由于正确的原因而引入的错误实现。 - JUST MY correct OPINION
我不会说“糟糕的__不正确__实现”。我更愿意说,该实现仍然存在危险陷阱,让不谨慎的人跌入其中。这种情况在抛出异常和捕获异常两方面都存在。如果我没记错的话,Java自己的Map实现中有一些代码可以捕获SecurityException并继续执行某些条件为false的操作,从而丢弃了安全问题。对于捕获者来说,陷阱就是对异常的来源(因此也就是确切含义)做出不正确和不必要的假设。对于抛出者来说,显然的陷阱是不知道所有可能的捕获者会期望什么。 - Erwin Smout

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接