既然可以捕获 Throwables,为什么还要在 Java 中捕获 Exception 呢?

最近,我们遇到了一个 Java 服务器应用程序抛出错误的问题,因为 Error 是 Throwable 的一个独立子类,我们只捕获异常。

我们通过捕获 Throwables 而不是 Exception 解决了当前的问题,但这让我思考为什么要捕获 Exception 而不是 Throwables,因为这样就会错过错误。

既然可以捕捉 Throwables,为什么还要捕捉 Exception呢?

27967 次浏览

来自 JavaAPI 文档:

Exception及其子类是 Throwable的一种形式,它指示合理的应用程序可能想要捕获的条件。

ErrorThrowable的一个子类,它指示一个合理的应用程序不应该试图捕捉的严重问题。

错误通常是低级别的(例如,由虚拟机引发) ,应用程序不应该捕获这些错误,因为合理的延续可能是不可能的。

通常错误是您不可能从中恢复的问题,比如 输出记忆错误。抓住它们无济于事,所以通常应该让它们逃跑,并关闭虚拟机。

通常在编程时,应该只捕获特定的异常(如 IOException)。在许多程序中,你可以看到一个非常顶级的

try {
...
} catch(Exception e) {
...
}

它捕获所有可以恢复的错误和所有指示代码中存在 bug 的错误,例如 InvalidArgumentExceptionNullPointerException。然后,您可以自动发送电子邮件,显示消息框或任何您喜欢的,因为 JavaVM 本身仍然工作良好。

Error得到的一切都是非常糟糕的东西,你不能做任何反对。问题是,捕获 OutOfMemoryErrorVirtualMachineError是否有意义。(这是 JavaVM 本身的一个错误,可能你甚至不能显示一个消息框或发送一封电子邮件)

您可能不应该从 Error派生类,而应该从 ExceptionRuntimeException派生。

稍微偏离了主题,但是您可能还想了解 这篇非常好的文章关于异常的内容。

这一切都取决于你如何处理一个错误,一旦你抓住了它。一般来说,捕捉错误可能不应该被视为“正常”异常流的一部分。如果你确实发现了一个错误,你不应该考虑“像什么都没有发生一样继续下去”,因为 JVM (和各种库)会使用 Error 作为一种信号,表明“真正严重的事情发生了,我们需要尽快关闭”。一般来说,当他们告诉你末日将至的时候,最好还是听他们的。

另一个问题是,错误的可恢复性与否可能取决于特定的虚拟机,而虚拟机是您可以控制或不能控制的。

也就是说,在一些极端情况下,捕获 Error (或者至少某些子类)是安全和/或可取的:

  • 有些情况下,你确实想要停止正常的流程: 例如,如果你在一个 Servlet 中,你可能不希望 Servlet 运行程序的默认异常处理程序告诉世界你已经有了一个 OutOfMemory 错误,不管你是否可以从中恢复。
  • 偶尔,在 JVM 可以从错误原因中干净地恢复的情况下,会抛出一个 Error。例如,如果在尝试分配数组时发生 OutOfMemory 错误,至少在 Hotspot 中,似乎可以安全地从这个错误中恢复。(当然,还有其他情况下,在不安全的情况下,可能会抛出 OutOfMemory 错误。)

所以底线是: 如果您确实捕获了 Throwable/Error 而不是 Exception,那么它应该是 定义明确的情况下,你知道你在“做一些特别的事情”

编辑: 可能这是显而易见的,但我忘了说,在实践中,JVM 实际上可能不会调用 catch 子句对一个错误。我确实见过 Hotspot 巧妙地掩盖了捕捉某些 OutOfMemory 错误和 NoClassDefFoundError 的尝试。

我知道这可能有违直觉,但正因为您可以捕获各种异常和抛物线及错误,所以 没有意味着您应该这样做。

过于积极地捕捉 java.lang。异常可能导致应用程序中出现一些严重的错误——因为意外的异常从来不会冒泡,在开发/测试过程中从来不会被捕获,等等。

最佳实践: 只接球

  1. 你能处理的例外
  2. 需要捕捉的异常

不要 永远捕捉 ThrowableError,你一般也不应该简单地捕捉一般的 ExceptionError通常是大多数合理的程序不可能从中恢复的东西。如果您知道正在发生什么,您可能能够从一个特定的错误中恢复,但是在这种情况下,您应该捕获 只有那个特定的错误,而不是一般的所有错误。

一个很好的理由不赶上 Error是因为 ThreadDeathThreadDeath是一个相当正常的事件,理论上可以从任何地方抛出(其他进程,如 JVM 本身可以生成它) ,它的全部意义就是杀死线程。显然,ThreadDeathError而不是 Exception,因为太多的人捕捉所有的 Exception。如果你曾经抓住 ThreadDeath,你 必须的重新抛出它,使你的线程实际上死亡。

如果您对源代码有控制权,那么它可能应该被重新构造为抛出 Exception而不是 Error。如果你不这样做,你可能应该打电话给供应商并投诉。Error应该只保留给那些终端的、无法从中恢复的东西。

我会走一条与众不同的路线。

在许多情况下,您希望捕捉 Throwable (主要是为了记录/报告发生了不好的事情)。

但是 ,您需要小心并重新抛出您无法处理的任何内容。

ThreadDeath 尤其如此。

如果你曾经抓到过 Throwable,一定要做到以下几点:

try {
...
} catch (SomeExceptionYouCanDoSomethingWith e) {
// handle it
} catch (ThreadDeath t) {
throw t;
} catch (Throwable t) {
// log & rethrow
}

这篇文章不会让“被检查的异常是坏的”的人高兴。然而,我的答案是基于 Java 异常是如何被创建语言的人定义的。

快速参考图表:

  • 可以扔,千万别接住
  • 错误-指示虚拟机错误-永远不要捕获这个
  • RuntimeException-表示程序员错误-永远不要捕获此错误
  • 例外,千万别接这个

不应该捕获 Exception 的原因是它捕获所有子类,包括 RuntimeException。

您不应该捕获 Throwable 的原因是它捕获了所有的子类,包括 Error 和 Exception。

上述“规则”也有例外(并非双关语) :

  • 正在使用的代码(来自第三方)引发 Throwable 或 Exception
  • 您正在运行不受信任的代码,如果程序引发异常,这些代码可能会导致程序崩溃。

对于第二个异常,通常只需要将 main、事件处理代码和线程包装到 Throwable,然后检查异常的实际类型并适当地处理它。

至少有一种情况下,我认为你可能必须捕获一个可抛出的或一般的异常-如果你正在运行一个单独的线程来执行任务,你可能想知道如果“运行”方法的线程已经捕获了一些异常或没有。在这种情况下,您可能会这样做:


public void run() {
try {
...
}
catch(Throwable t) {
threadCompletionError = t;
}
}

我真的不确定这是否是最好的方法,但它确实有效。JVM 引发了一个“ ClassNotfound”错误,这是一个错误,而不是一个异常。如果我让异常被抛出,我不确定如何在调用线程中捕获它(可能有一个方法,但我还不知道它)。

至于 ThreadDeath 方法,不要调用“ Thread.stop ()”方法。调用 Thread.stop 并让线程检查它是否被某人中断。

一般来说,如果能够正确地报告错误,那么尝试捕获错误是合理的。

但是,我相信在某些情况下,捕获一个错误而不报告它是合适的。我指的是不满意的 LinkError。在 JAI 中,由于性能原因,库使用一些本地库来实现大多数操作符,但是如果库无法加载(不存在,格式错误,不支持平台) ,库仍然可以运行,因为它将回落到只支持 Java 的模式。

抓住 错误是没有意义的。

错误用于指示应用程序中出现了严重错误,应该重新启动它。

例如,一个常见的错误是

java.lang.OutOfMemoryError

当这种情况发生时,你可以做 没什么。已经太晚了,JVM 已经用尽了所有的选项来获得更多的内存,但这是不可能的。

请参阅另一个答案以了解关于 三种例外的更多信息。

很多其他的答案都太狭隘了。

正如他们所说,如果您正在编写应用程序代码,就不应该捕获 Throwable。您对此无能为力,因此最好允许周围的系统(JVM 或框架)来处理这些问题。

但是,如果您正在编写“系统代码”,比如框架或其他低级代码,那么您可能非常希望捕获 Throwable。原因是要 尝试报告异常,可能是在一个日志文件中。在某些情况下,您的日志记录会失败,但在大多数情况下,它会成功,您将获得解决问题所需的信息。完成日志记录后,应该重新抛出、终止当前线程或退出整个 JVM。

为什么不把他们都抓起来?然后记录它们,至少你知道你有一个错误。所以抓住 Throwable/s 比抓住 Exception/s 要好。