嵌套的try/catch块是否更好?

41

在Java中使用Reader和Stream时,让我感到困扰的一件事情是close()方法可能会引发异常。由于把close方法放在finally块中是一个好习惯,这就需要我们处理一些尴尬的情况。我通常会使用以下代码:

FileReader fr = new FileReader("SomeFile.txt");
try {
    try {
        fr.read();
    } finally {
        fr.close();
    }
} catch(Exception e) {
    // Do exception handling
}

但我也看到过这种结构:

FileReader fr = new FileReader("SomeFile.txt");
try {
    fr.read() 
} catch (Exception e) {
    // Do exception handling
} finally {
    try {
        fr.close();
    } catch (Exception e) {
        // Do exception handling
    }
}

我更喜欢第一种结构,因为只有一个catch块,看起来更优雅。是否确实有理由偏爱第二种或其他结构?

更新:如果我指出read和close都只会抛出IOException,是否会有所不同?因此,如果读取失败,则关闭也可能因相同的原因而失败。


12
最糟糕的是 close() 方法可能会抛出异常。如果发生异常,除了关闭流以外,你还能做什么呢?但是关闭时又会抛出另一个异常,所以你应该关闭流,这将会......我不喜欢 Java 因为这些琐碎的事情。 - OregonGhost
1
@OregonGhost - 如果用户正在使用一组流并且其中一个没有成功关闭,会发生什么?这可能意味着所有其他流都处于不可用状态,并且应该进行测试。如果忽略,则它们可能无法正常使用,用户继续工作但无法保存。 - Rontologist
@Rontologist:我的观点是,当抛出异常时,你除了关闭它之外什么也做不了,那么为什么要在关闭时抛出异常呢?在我看来,关闭不应该失败,因为你无法对此做任何事情,也无法摆脱它。在关闭后,流无论成功与否都是坏的。在.NET中,Close()不会抛出异常。 - OregonGhost
1
@OregonGhost:如果您正在使用单个流,则是有效的。但是,当有多个流在运行时,触发对其他流进行合理性检查以确定是否需要用户干预是一个绝佳的机会。 - Rontologist
@Rontologist:我从未见过这样的代码。大多数情况下,当一个流失败时,要么关闭(因为即使其他流正常,你也无法继续),要么继续并从其他流中获取异常信息。 - OregonGhost
11个回答

26

很遗憾,第一个例子存在一个大问题,即如果在读取操作之后或期间发生异常,则会执行finally块。目前为止都还好。但是,如果接下来fr.close()引发另一个异常呢?这将“击败”第一个异常(有点像在finally块中放置return),导致您失去了关于实际导致问题的所有信息

你的finally块应该使用:

IOUtil.closeSilently(fr);

这个实用方法只是执行:

public static void closeSilently(Closeable c) {
    try { c.close(); } catch (Exception e) {} 
} 

3
抢我一步了。提醒一下:如果您正在使用记录子系统,我建议在 closeSilently 中添加日志信息,以便在异常发生时记录一个带有“已抑制”注释的日志。对异常不做任何响应可能会隐藏后续重要的诊断信息。 - James Schek
2
非常正确。也许省略日志语句是我先回答的一个因素? - oxbow_lakes
1
吞咽这样的异常可能是危险的。至少在catch中记录堆栈跟踪。根据情况,将异常包装在运行时异常中可能是更好的方法。 - Rontologist
我不喜欢这个解决方案。如果关闭抛出异常,但读取没有呢?没有异常传递给调用者,也没有错误处理发生。对于读取器来说可能还好,但是对于写入器呢? - McDowell
为了避免丢失第一个异常中的信息,我认为您可以将它们链接在一起:http://download.oracle.com/javase/tutorial/essential/exceptions/chained.html - Vimes
显示剩余5条评论

7

我会选择第一个例子。

如果close抛出异常(实际上对于FileReader来说几乎不可能发生),处理这种情况的标准方式是抛出适合调用者的异常,因为close异常几乎肯定比使用资源时遇到的任何问题更重要。如果您的异常处理想法是调用System.err.println,则第二种方法可能更合适。

有一个问题需要考虑:异常应该抛多远。ThreadDeath应该总是重新抛出,但finally中的任何异常都会阻止这一点。类似地,Error应该抛出比RuntimeException更远,而RuntimeException应该抛出比受检异常更远。如果您真的想这样做,可以编写代码遵循这些规则,然后使用“执行周围”习语进行抽象。


4
我更喜欢第二个。为什么?如果read()close()都抛出异常,其中一个可能会丢失。在第一种情况下,close()的异常将覆盖read()的异常,而在第二种情况下,close()的异常将被单独处理。
从Java 7开始,try-with-resources结构使这变得更加简单。要读取并忽略异常:
try (FileReader fr = new FileReader("SomeFile.txt")) {
    fr.read();
    // no need to close since the try-with-resources statement closes it automatically
}

使用异常处理:

try (FileReader fr = new FileReader("SomeFile.txt")) {
    fr.read();
    // no need to close since the try-with-resources statement closes it automatically
} catch (IOException e) {
    // Do exception handling
    log(e);
    // If this catch block is run, the FileReader has already been closed.
    // The exception could have come from either read() or close();
    // if both threw exceptions (or if multiple resources were used and had to be closed)
    // then only one exception is thrown and the others are suppressed
    // but can still be retrieved:
    Throwable[] suppressed = e.getSuppressed(); // can be an empty array
    for (Throwable t : suppressed) {
        log(suppressed[t]);
    }
}

只需要一个try-catch块就可以安全地处理所有异常。如果你愿意,仍然可以添加一个finally块,但没有必要关闭资源。


2
如果读取关闭都抛出异常,选项1会隐藏读取抛出的异常。因此,第二个选项执行更多的错误处理。然而,在大多数情况下,仍然会首选第一种选项。
  1. 在许多情况下,您不能在生成异常的方法中处理异常,但仍必须将流处理封装在该操作内。
  2. 尝试向代码添加写入器,看看第二种方法变得多么冗长。
如果需要传递所有生成的异常,则可以使用此方法

1
据我所见,不同水平的使用不同的异常和原因,而catch(Exception e)模糊了这一点。多级别的唯一目的是区分您的异常以及您对它们所采取的操作:
try
{
  try{
   ...
  }
   catch(IOException e)
  {
  ..
  }
}
catch(Exception e)
{
  // we could read, but now something else is broken 
  ...
}

1

我通常会这样做。首先,定义一个基于模板方法的类来处理try/catch混乱情况

import java.io.Closeable;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public abstract class AutoFileCloser {
    private static final Closeable NEW_FILE = new Closeable() {
        public void close() throws IOException {
            // do nothing
        }
    };

    // the core action code that the implementer wants to run
    protected abstract void doWork() throws Throwable;

    // track a list of closeable thingies to close when finished
    private List<Closeable> closeables_ = new LinkedList<Closeable>();

    // mark a new file
    protected void newFile() {
        closeables_.add(0, NEW_FILE);
    }

    // give the implementer a way to track things to close
    // assumes this is called in order for nested closeables,
    // inner-most to outer-most
    protected void watch(Closeable closeable) {
        closeables_.add(0, closeable);
    }

    public AutoFileCloser() {
        // a variable to track a "meaningful" exception, in case
        // a close() throws an exception
        Throwable pending = null;

        try {
            doWork(); // do the real work

        } catch (Throwable throwable) {
            pending = throwable;

        } finally {
            // close the watched streams
            boolean skip = false;
            for (Closeable closeable : closeables_) {
                if (closeable == NEW_FILE) {
                    skip = false;
                } else  if (!skip && closeable != null) {
                    try {
                        closeable.close();
                        // don't try to re-close nested closeables
                        skip = true;
                    } catch (Throwable throwable) {
                        if (pending == null) {
                            pending = throwable;
                        }
                    }
                }
            }

            // if we had a pending exception, rethrow it
            // this is necessary b/c the close can throw an
            // exception, which would remove the pending
            // status of any exception thrown in the try block
            if (pending != null) {
                if (pending instanceof RuntimeException) {
                    throw (RuntimeException) pending;
                } else {
                    throw new RuntimeException(pending);
                }
            }
        }
    }
}

请注意 "pending" 异常 - 这会处理在关闭期间抛出的异常掩盖我们真正关心的异常的情况。
finally 首先尝试从任何装饰流的外部关闭,因此如果您有一个包装 FileWriter 的 BufferedWriter,则首先尝试关闭 BuffereredWriter,如果失败,则仍然尝试关闭 FileWriter 本身。
您可以按以下方式使用上述类:
try {
    // ...

    new AutoFileCloser() {
        @Override protected void doWork() throws Throwable {
            // declare variables for the readers and "watch" them
            FileReader fileReader = null;
            BufferedReader bufferedReader = null;
            watch(fileReader = new FileReader("somefile"));
            watch(bufferedReader = new BufferedReader(fileReader));

            // ... do something with bufferedReader

            // if you need more than one reader or writer
            newFile(); // puts a flag in the 
            FileWriter fileWriter = null;
            BufferedWriter bufferedWriter = null;
            watch(fileWriter = new FileWriter("someOtherFile"));
            watch(bufferedWriter = new BufferedWriter(fileWriter));

            // ... do something with bufferedWriter
        }
    };

    // .. other logic, maybe more AutoFileClosers

} catch (RuntimeException e) {
    // report or log the exception
}

使用这种方法,您永远不必担心try/catch/finally来处理关闭文件的问题。
如果这对您的使用来说太重了,至少考虑遵循它使用的try/catch和“pending”变量方法。

0
在某些情况下,无法避免使用嵌套的Try-Catch。例如,当错误恢复代码本身可能会抛出异常时。但是为了提高代码的可读性,您可以将嵌套块提取为自己的方法。请查看this博客文章以获取更多关于嵌套Try-Catch-Finally块的示例。

0

第一个构造函数可以在外部 try 块中移动,没有问题。可能外部的 try 块在不同的方法中。当然,在 FileRead 中打开底层流并从构造函数返回之前抛出了未经检查的异常,因此底层流没有关闭。 - Tom Hawtin - tackline

0
有时候嵌套的 try-catch 并不是首选,可以考虑以下方式:
try{
 string s = File.Open("myfile").ReadToEnd(); // my file has a bunch of numbers
 // I want to get a total of the numbers 
 int total = 0;
 foreach(string line in s.split("\r\n")){
   try{ 
     total += int.Parse(line); 
   } catch{}
 }
catch{}

这可能是一个不好的例子,但有时候你需要嵌套的 try-catch。


0

我使用的标准约定是不允许异常逃逸到finally块外。

这是因为如果一个异常已经在传播,那么从finally块抛出的异常将会覆盖原始异常(因此丢失)。

在99%的情况下,这不是你想要的,因为原始异常很可能是你问题的根源(任何次要的异常可能是第一次异常的副作用,但会遮盖你找到原始异常和真正的问题的能力)。

所以你的基本代码应该像这样:

try
{
    // Code
}
// Exception handling
finally
{
    // Exception handling that is garanteed not to throw.
    try
    {
         // Exception handling that may throw.
    }
    // Optional Exception handling that should not throw
    finally()
    {}
}

如果在 finally 块内部时 ThreadDeath 到达会怎样? - finnw

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