为什么要捕获特定的Java异常,除非需要特定的处理

  • 本文关键字:处理 异常 Java java
  • 更新时间 :
  • 英文 :


以下是从https://dzone.com/articles/9-best-practices-to-handle-exceptions-in-java

public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}

如果所有的catch处理程序本质上都在做同样的事情,我不明白为什么我们要分别捕获每个特定的异常,而不是简单地捕获通用的基类exception。

从本质上讲,如果您不在寻找特定异常的属性/方法/任何东西,为什么不直接捕获泛型异常呢?

我认为本文作者只是想先捕捉最具体的异常,然后再捕捉更通用的异常。这不仅是一种最佳实践,也是Java语言的要求,因为由于定义了不可访问的代码块,否则代码将无法编译。

在您引用的特定代码块中,这是不必要的,您还不如只捕获IOException,因为FileNotFoundException扩展了它+一旦捕获到异常,预期的行为完全相同。

对于给定的样本,有两个catch块确实是多余的。对于CCD_ 5,它可以在单个CCD_。

但让我们假设我们有一个这样的方法:

public final Optional<Status> doSomething( final String... arguments )
throws IOException, SQLException, PatternSyntaxException
{
…
}

我们不在乎这个方法到底在做什么,我们只是假设它不会检查arguments中的null——这意味着在某些情况下我们可以看到NullPointerException

现在让我们使用这个方法,使用您建议的错误处理:

try
{
final var status = doSomething( arguments ).get();  
}
catch( final Exception e ) // Not good!
{
handleExpectedException( e );
}

看起来很好,但调用handleExcpectedException()时也可能出现一些"意外"异常:使用NullPointerExceptionNoSuchElementException

好的,第二次尝试:

try
{
final var status = doSomething( arguments ).get();  
}
catch( final IOException e )
{
handleExpectedException( e );
}
catch( final SQLException e )
{
handleExpectedException( e );
}
catch( final PatternSyntaxException e )
{
handleExpectedException( e );
}
catch( final NullPointerException e )
{
handleUnexpectedException( e );
}
catch( final NoSuchElementException e )
{
handleUnexpectedException( e );
}

坦白地说,这是一大堆台词,几乎没有任何意义。因此,Java 5(或Java 6?)引入了一种方法来组合以相同方式处理的多个异常的catch

try
{
final var status = doSomething( arguments ).get();  
}
catch( final IOException | SQLException | PatternSyntaxException e )
{
handleExpectedException( e );
}
catch( final NullPointerException | NoSuchElementException e )
{
handleUnexpectedException( e );
}

但是,我们为什么要列出例外情况呢?为什么不只拥有两个类别并像这样处理它们:

try
{
final var status = doSomething( arguments ).get();  
}
catch( final RuntimeException e )
{
handleUnexpectedException( e );
}
catch( final Exception e )
{
handleExpectedException( e );
}

这是因为我们希望意外的异常在发生时会出现!我们希望(至少,我希望)一个行为完全出乎意料的程序会很快失败!而且声音很大!

doSomething()签名中的throws子句告诉我们,它可能会抛出三种不同类型的异常(通常,相应的文档会告诉我们为什么)。所以我们可以准备我们的代码来处理它们。只需记录它们并终止是最简单的方法,但也许我们知道如何从错误状态中恢复并可以重试,或者我们有其他处理方法,或者其他什么方法。

NullPointerException要么表明doSomething()的代码中存在编程错误,要么我们没有正确阅读文档并向其中输入null。两者都是运行时无法修复的。

NoSuchElementException显然是由我们代码中的一个错误引起的:doSomething()返回Optional的一个实例,该实例可以为空;对结果调用Optional::get而不检查它绝对是一个错误!我想尽快看到这个bug,以便能够尽快修复它!

Exceptioncatch块可能会吞下这些异常,它们永远不会出现——我永远找不到我的程序有时会返回意外结果的原因…


因此,建议只捕获在该级别预期的异常(要么用throws子句声明,要么在文档中描述),并且可以在当前catch块中处理。

"处理"异常意味着系统将恢复到稳定状态!

catch块中记录一个FileNotFoundException,然后在该catch块(如(不存在!)文件)被找到并打开后继续,这是在调用desaster!

如果您只是因为想丰富错误消息而在本地捕获异常,则不应将其记录下来;只需包装并重新投掷:

…
catch( final FileNotFoundException e )
{
final var message = "MatrixReport output file %s could not be found!".formatted( e.getMessage() );
throw new MyApplicationError( message, e );
}

因此,最后,当我使用doSomething()(不修复已经提到的错误)时,我的代码可能是这样的(假设FileNotFoundException有文档记录):

try
{
final var status = doSomething( arguments ).get();  
}
catch( final FileNotFoundException e )
{
final var message = "MatrixReport output file %s could not be found!".formatted( e.getMessage() );
throw new MyApplicationError( message, e );
}
catch( final IOException | SQLException | PatternSyntaxException e )
{
handleExpectedException( e );
}

好的,我们只处理我们可以处理的异常,让其他的冒泡……但现在谁来处理这些异常?

当这种情况在主线程中发生时,终止它的异常将打印到stdout。太棒了其他线程呢?

通常,这些都会悄无声息地死去…

因此,您经常看到在线程的run()方法中为RuntimeException设置catch块的建议,如下所示:

public final void run()
{
try
{
// Do this thread's work …
}
catch( final RuntimeException e )
{
log.error( e );
}
}  

但这可以更容易地实现:您可以将UncaughtExceptionHandler实例设置为Thread!请参阅JavaDoc。您甚至可以通过将线程分配给一个提供UncaughtExceptionHandler的特定ThreadGroup(您必须实现自己的扩展ThreadGroup的类),或者您有一个catch0来为您分配…


哦,在我忘记之前:我引入handleExpectedException()是有原因的,而不是简单地使用log.error():如果你不修复异常所指示的状态,你可能无法继续正常的代码流!记录异常是而不是处理它!

这意味着handleException()在大多数情况下不会定期返回;也许它也抛出了一个MyApplicationError的实例,或者它会调用System::exit(当你在web应用程序中时,真的很讨厌……不要这么做!)或者任何合适的…

考虑到在那个特定的例子中,处理异常没有区别,所以在它们之间进行区分没有任何用处。由于FileNotFoundException是IOException子类型,所以只捕获IOException就可以了。

现在,一般来说,如果您不知道如何处理捕获的异常。不要在方法的调用者上推送原始异常。不要添加";投掷某物;方法签名。除非您能够添加异常的具体原因,否则甚至不要麻烦记录异常。您应该做的是将它们重新命名为RuntimeException。像这个例子:

public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
}
catch (IOException e) {
throw new RuntimeException("An error handling './tmp.txt'", e);
}
}

那么这里面的逻辑是什么

如果你现在不知道如何处理这个IOException-你的方法的任何调用者都将对如何处理它知之甚少。因此,你不应该用不必要的锅炉板来打扰人们,这就是为什么没有"IOException";抛出IOException";条款相反,捕获的异常由传统的RuntimeException包装。也没有记录我们这样做。

谁来处理RuntimeException

代码中的每个线程都应该有一个顶级异常处理程序。没有什么智能,只是一个通用的处理程序:

try {
// my app code here
} 
catch(MyCustomException exc) {
// do smart stuff here
}
catch(RuntimeExceptio exc) {
log.error("An unexpected has happened", exc);
// rethrow or consume here
}

因此;不可恢复的";异常被推迟到最高捕获级别。通常的登录和重新处理异常的做法只会导致日志被同一个异常多次污染。考虑到所有Java异常在它们被引发的地方都携带着完美的信息,除非你能向日志中添加有用的东西,否则不要麻烦记录它们,除非是在顶层。

最后,为什么不捕捉所有的异常、错误或抛出

同样,作为一般建议,您的代码应该只捕获(并重新抛出)由您调用的代码直接引发的异常。泛型异常/可抛出块将捕获您没有机会正确处理的所有其他异常。像OutOfMemoryException这样的系统内容。这些异常是不可恢复的,您使用它们只会导致应用程序不那么稳定,更难调试。

您说得对,这在编写时毫无意义。

他们可能应该编写log.error("TODO: Handle FileNotFound", e);// TODO: Handle missing files,这样你就不会认为log.error(e);是最终目标,而不是替身。

当你看到建议的重写时,它更有意义:

public void closeResourceInFinally() {
FileInputStream inputStream = null;
try {
File file = new File("./tmp.txt");
inputStream = new FileInputStream(file);
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.error(e);
}
}
}
}

看来,作者希望在初始化时捕获一个FileNotFoundException,在关闭时捕获IOException0。

如果第一个例子只捕获了IOException,因为它的数量相同,你会不会问";等等,为什么它现在突然捕获FileNotFoundException">

通过在原始示例中包含这两个,可以更容易地看到第一个映射到第二个。

最新更新