没有 catch 块的 finally 块是否是 Java 的反模式?

47

我刚刚经历了一个非常痛苦的故障排除过程,排查的代码看起来像这样:

try {
   doSomeStuff()
   doMore()
} finally {
   doSomeOtherStuff()
}
因为doSomeStuff()引发了异常,而这又导致doSomeOtherStuff()也抛出了异常,所以这个问题很难进行故障排除。try语句块中的第二个异常被传递到了我的代码中,但它没有处理来自doSomeStuff()的第一个异常,而那才是问题的真正根源。
如果代码改成了下面这样,问题就会容易得多:
try {
    doSomeStuff()
    doMore()
} catch (Exception e) {
    log.error(e);
} finally {
   doSomeOtherStuff()
}

所以,我的问题是:

在没有任何catch块的情况下使用finally块是否是一个众所周知的Java反模式?(它显然似乎是一个不易察觉的子类,属于明显众所周知的反模式“不要吞噬异常!”)


请参考关于C#中没有catch的try-finally的类似问题[https://dev59.com/bnVC5IYBdhLWcg3w-mVO],相同的论点适用。 - avandeursen
12个回答

65

一般来说,不是反模式。finally块的作用是确保无论是否抛出异常都能清理现场。异常处理的整个目的在于,如果你无法处理它,你就让它通过相对干净的带外信号传递到可以处理它的人那里。如果你需要确保在抛出异常时清理现场,但又不能在当前范围内正确处理异常,则这正是正确的做法。但您可能需要更加小心地确保finally块不会抛出异常。


但在这种情况下,根本原因异常没有被允许上升,因为finally步进它(从而导致它被吞噬)。 - Jared
好的,但那只是一个特例。你是在问try-finally没有catch是否总体上是一种反模式。 - dsimcha
2
同意,dsimcha。我希望有一致的共识,一般情况下这不是反模式。也许一个反模式是“finally块抛出异常”? - Jared
3
好的回答,我只想补充一点,这通常被称为“回避”异常。 - Oscar Gomez
@dsimcha,+1,完全同意你的观点;你能否分享一个例子,以便我们可以了解程序员在异常发生时运行清理的情况。我想到了IOException,但流无法打开,因此不需要清理代码。 - Ankit

30

我认为真正的“反模式”是在finally块中执行可能会抛出异常的操作,而不是没有捕获异常。


17

完全不是这样。

问题出在 finally 代码块内部。

请记住,finally 代码块总是会被执行的。将可能会抛出异常的代码放在其中是有风险的(正如您刚刚目睹的那样)。


11

使用 try-finally 而没有 catch 是没有任何问题的。考虑以下代码:

InputStream in = null;
try {
    in = new FileInputStream("file.txt");
    // Do something that causes an IOException to be thrown
} finally {
    if (in != null) {
         try {
             in.close();
         } catch (IOException e) {
             // Nothing we can do.
         }
    }
}

如果抛出异常,并且此代码不知道如何处理它,则应该将异常沿着调用堆栈向上传递给调用方。在这种情况下,我们仍然希望清理流,因此我认为有一个没有catch的try块是完全有意义的。


1
如果您从此示例中删除在finally中嵌套的catch,则如果try引发了IOException并且close()也引发了IOException,您无法从堆栈跟踪中确定问题的根本原因是最初的IOException。这只是那些“是的,这将始终很困难”的情况之一吗? - Jared
1
@Jared:是的,这将永远很难。 :) - Bryan Kyle
1
请注意 - 当您从 finally 块中删除 catch 时,您基本上表示您不关心异常来自哪里... - Kevin Day

5

我认为这远非反模式,而是在方法执行期间必须释放资源时经常采用的一种做法。

处理文件句柄(写入)时,我会使用IOUtils.closeQuietly方法在关闭之前刷新流,该方法不会抛出异常:


OutputStream os = null;
OutputStreamWriter wos = null;
try { 
   os = new FileOutputStream(...);
   wos = new OutputStreamWriter(os);
   // 大量代码
wos.flush(); os.flush(); finally { IOUtils.closeQuietly(wos); IOUtils.closeQuietly(os); }

我之所以喜欢这样做,是因为以下原因:

  • 忽略关闭文件时出现的异常是不完全安全的 - 如果有未写入文件的字节,则文件可能不处于调用者所期望的状态;
  • 因此,如果在flush()方法期间引发异常,则该异常将传播给调用者,但我仍将确保所有文件都已关闭。方法IOUtils.closeQuietly(...)比相应的try ... catch ... ignore me块更简洁;
  • 如果使用多个输出流,则flush()方法的顺序很重要。通过将其他流作为构造函数传递来创建的流应首先刷新。close()方法也是一样,但我认为flush()更清晰。

1
我在以下形式中使用try/finally:
try{
   Connection connection = ConnectionManager.openConnection();
   try{
       //work with the connection;
   }finally{
       if(connection != null){
          connection.close();           
       }
   }
}catch(ConnectionException connectionException){
   //handle connection exception;
}

我更喜欢这种方式而不是使用try/catch/finally(+在finally中嵌套try/catch)。 我认为这种方式更简洁,而且不会重复捕获异常。


这不会遇到与在try中的异常被finally吞噬的问题相同的问题吗? - Jared

1
try {
    doSomeStuff()
    doMore()
} catch (Exception e) {
    log.error(e);
} finally {
   doSomeOtherStuff()
}

也不要这样做...你只是隐藏了更多的错误(虽然并不完全是隐藏...但让处理它们变得更加困难)。当你捕获异常时,你也会捕获任何类型的RuntimeException(比如NullPointer和ArrayIndexOutOfBounds)。

一般来说,捕获你必须捕获的异常(已检查异常),并在测试时处理其他异常。RuntimeExceptions被设计用于程序员错误 - 而程序员错误是不应该发生在经过适当调试的程序中的事情。


没错...同意。这继续指向真正的反模式是一个抛出异常的finally块。 - Jared

1
在我看来,finallycatch 一起使用时更多的是表示出现了某种问题。资源惯用语非常简单:
acquire
try {
    use
} finally {
    release
}

在Java中,你几乎可以从任何地方抛出异常。通常获取会抛出一个已检查的异常,处理它的明智方法是在整个代码块周围放置一个catch。不要尝试一些丑陋的空值检查。
如果你真的很挑剔,你应该注意异常之间的隐含优先级。例如ThreadDeath应该覆盖所有异常,无论它来自获取/使用/释放。正确处理这些优先级是不美观的。
因此,使用“执行周围”惯用语法将资源处理抽象化。

1
我认为没有 catch 块的 try 块是一种反模式。说“不要在没有 catch 的情况下使用 finally”是“不要在没有 catch 的情况下使用 try”的子集。

3
不值得负面投票,但它并不是反模式。finally块允许某段代码始终运行,例如关闭资源或释放锁定,这些操作可能不会引发任何异常。 - Chii
我的观点是,无论是否有Finally,缺少Catch都是上述反模式。吞噬异常是导致调试问题的原因,而Finally中可能存在异常代码更加恶化了这个问题。 - billjamesdev
1
就“踩票”而言,很多人一旦看到第一个踩票就会跟着堆上去。他们觉得有时候应该踩票,但又不想自己做决定。 - billjamesdev

0

try-finally 可以帮助您减少复制粘贴代码,特别是当一个方法有多个 return 语句时。请考虑以下示例(Android Java):

boolean doSomethingIfTableNotEmpty(SQLiteDatabase db) {
    Cursor cursor = db.rawQuery("SELECT * FROM table", null);
    if (cursor != null) { 
        try {
            if (cursor.getCount() == 0) { 
                return false;
            }
        } finally {
            // this will get executed even if return was executed above
            cursor.close();
        }
    }
    // database had rows, so do something...
    return true;
}

如果没有 finally 子句,你可能需要在 return false 之前和包围的 if 子句之后写两次 cursor.close()

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