Error handling mechanisms like exceptions cause deviations in the usual control flow of the program.

异常等错误处理机制会导致程序的常规控制流出现偏差。

These deviations make it harder to reason about the program and are often the source of bugs in the program.

这些偏差使程序的推理变得更加困难,而且往往是程序中的错误的来源。

Java provides exception handling via the Try-Catch-Finally statement

Java通过Try-Catch-Finally语句提供异常处理。

They also found that sometimes for correct handling of exceptions you need to nest Try-Catch-Finally blocks, however developers avoid doing so as it affects readability of the code.

他们还发现,有时为了正确处理异常,你需要嵌套Try-Catch-Finally块,但开发人员避免这样做,因为这影响了代码的可读性。

In this article, we look at various examples of nested Try-Catch-Finally statements in Java and how and when to avoid them.

在这篇文章中,我们看一下Java中嵌套的Try-Catch-Finally语句的各种例子,以及如何和何时避免它们。

Nested Try-Finally Example

Let us start with the following example from the paper by Wiemer and Necula:

Connection cn;
PreparedStatement ps;
ResultSet rs;
cn = ConnectionFactory.getConnection(/* ... */);
try {
    StringBuffer qry = ...; // do some work
    ps = cn.prepareStatement(qry.toString());
    try {
        rs = ps.executeQuery();
        try {
            ... // do I/O-related work with rs
        } finally {
            rs.close();
        }
    } finally {
        ps.close();
    }
} finally {
    cn.close();
}

In this example, while using several resources (cn, ps and rs) simultaneously, it becomes necessary to nest Try-Finally blocks 3 levels deep to release them properly with the close() method.

在这个例子中,在同时使用几个资源(cn、ps和rs)时,有必要将 Try-Finally 块嵌套到3层深处,以便用 close() 方法正确释放它们。

The resulting code is not very readable and may be even confusing to some developers. An alternative is to do something like the following:

由此产生的代码可读性不强,甚至可能让一些开发人员感到困惑。一个替代方法是做如下的事情。

Connection cn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
    cn = ConnectionFactory.getConnection(/* ... */);
    StringBuffer qry = ...; // do some work
    ps = cn.prepareStatement(qry.toString());
    rs = ps.executeQuery();
    ... // do I/O-related work with rs
} finally {
    if (rs != null) try {rs.close()} catch (Exception e) {}
    if (ps != null) try {ps.close()} catch (Exception e) {}
    if (cn != null) try {cn.close()} catch (Exception e) {}
}

This solution uses null as a sentinel value. Although it is correct, it is more prone to error. e.g. it is easy to forget checking for a particular resource. In general, correctly dealing with N resources like this would require N nested Try-Finally blocks or a number of runtime checks.

这个解决方案使用 null 作为哨兵值。虽然它是正确的,但它更容易出错。例如,它很容易忘记检查某个特定的资源。一般来说,像这样正确地处理N个资源,需要N个嵌套的 Try-Finally 块或一些运行时检查。

Nested Try-Catch Example

Similar to the situation with a Try-Finally block, we may sometimes need to nest Try-Catch blocks. As an example, take a look at the code below (taken from this link):

与 Try-Finally 块的情况类似,我们有时可能需要嵌套 Try-Catch 块。作为一个例子,请看下面的代码(取自这个链接)。

try {
    transaction.commit();
} catch {
    logerror();
    try {
        transaction.rollback();
    } catch {
        logerror();
    }
}

While committing a transaction, if it fails we log the error and then try to recover by rolling back.

在提交事务时,如果失败了,我们会记录错误,然后尝试通过回滚来恢复。

However this rollback can fail itself which requires another nested Try-Catch block to handle correctly.

然而,这个回滚本身可能失败,这需要另一个嵌套的 Try-Catch 块来正确处理。

In fact a common use case that leads to a nested Try-Catch happens when error recovery from an exception itself may raise an another exception.

事实上,导致嵌套 Try-Catch 的一个常见的用例是,当从一个异常本身恢复错误时,可能会引发另一个异常。

Recently, I ran into a similar issue while trying to use the JRuby-Parser library to parse some Ruby code.

最近,我在试图使用 JRuby-Parser 库来解析一些 Ruby 代码时遇到了一个类似的问题。

The parser provided by the library has three compatibility versions for parsing different versions of Ruby (2.0, 1.9 and 1.8).

该库提供的解析器有三个兼容版本,用于解析不同版本的Ruby(2.0、1.9和1.8)。

Hence, to parse arbitrary Ruby code we needed to try them each one by one if the first one fails.

因此,为了解析任意的 Ruby 代码,我们需要在第一次失败的情况下,逐一尝试它们。

The parser throws can exception if it fails to parse the code, so I ended up writing something with two nested Try-Catch blocks as shown below:

如果解析器不能解析代码,它就会抛出异常,所以我最后写了两个嵌套的 Try-Catch 块,如下:

try {
    Node root = rubyParser.parse(f, new StringReader(fileContent), new ParserConfiguration(0, CompatVersion.RUBY2_0));
      root.accept(new RubyVisitor(calls));
} catch (Exception e) {
    try {
        Node root = rubyParser.parse(f, new StringReader(fileContent), new ParserConfiguration(0, CompatVersion.RUBY1_9));
        root.accept(new RubyVisitor(calls));
    }
    catch (Exception ex) {
        Node root = rubyParser.parse(f, new StringReader(fileContent), new ParserConfiguration(0, CompatVersion.RUBY1_8));
        root.accept(new RubyVisitor(calls));
    }
}

How to Avoid the Nesting?

The nested Try-Catch block above is actually totally avoidable. One way to safely rewrite it so that it doesn't use a nested Try-Catch is as follows:

上面这个嵌套的 Try-Catch 块其实是完全可以避免的。有一种方法可以安全地重写它,使其不使用嵌套的 Try-Catch,如下所示:

for(CompatVersion cv : cvarr) {
  try {
    root = rubyParser.parse(f, new FileReader(f), new ParserConfiguration(0, cv));
    break;
  }
  catch (Throwable e){
    Logger.getLogger(SourceAnalysis.class.getName()).log(Level.WARNING, "Parse error in file " +
          f + " and parser version " + cv, e);
  }
}

I refactored the code to move the Try-Catch inside a loop which tries different compatible versions of the parser.

我重构了代码,将 Try-Catch 移到一个循环中,该循环尝试了不同的兼容版本的分析器。

So one way to avoid the nesting is to change the control flow around the block.

因此,避免嵌套的一个方法是改变块周围的控制流。

Another good way (described here) to avoid a nested Try-Catch-Finally block is to see if we can combine them.

另一个避免嵌套的 Try-Catch-Finally 块的好方法(在此描述)是看我们是否可以将它们结合起来。

This is more useful if the nesting is only used to catch different kinds of exceptions and can be combined as follows:

如果嵌套只用于捕捉不同种类的异常,这就更有用了,可以按以下方式组合。

try {
    //code
} catch (FirstKindOfException e) {
    //do something
} catch (SecondKindOfException e) {
    //do something else
}

However, in some cases is may not be possible to avoid the nesting (e.g. when error recovery itself can throw exceptions).

然而,在某些情况下,可能无法避免嵌套(例如,当错误恢复本身可以抛出异常)。

Even then we can improve the readability of the code by using early exists and moving the nested part into a new method.

即使这样,我们也可以通过使用早期存在和将嵌套部分移到一个新方法中来提高代码的可读性。

Thus, for the transaction error recovery example earlier we can write it without nesting in the following way:

因此,对于前面的交易错误恢复的例子,我们可以不通过嵌套的方式来写,方法如下。

try {
    transaction.commit();
} catch {
    callerrorrecovery();
}
private void callerrorrecovery() {
    try {
        transaction.rollback();
    } catch {
        logerror();
    }
}

Extracting the nested part as a new method will always work for any arbitrarily nested Try-Catch-Finally block.

提取嵌套部分作为一个新的方法,对于任何任意嵌套的 Try-Catch-Finally 块都是有效的。

So this is one trick that you can always use to improve the code.

因此,这是一个你可以随时用来改进代码的技巧。

Do you consider nested Try-Catch-Finally blocks to be a bad practice? Or, if you know some other ways to avoid them do let us know in comments.

你认为嵌套的 Try-Catch-Finally 块是一种不好的做法吗?或者,如果你知道一些其他方法来避免它们,请在评论中告诉我们。


本文来源:

Avoiding Nested Try-Catch in Java | Veracode

标签: none

评论已关闭