【问题标题】:Is it OK to catch Throwable for performing cleanup? [duplicate]可以捕获 Throwable 进行清理吗? [复制]
【发布时间】:2013-06-27 03:10:46
【问题描述】:

举个例子:

public List<CloseableThing> readThings(List<File> files) throws IOException {
    ImmutableList.Builder<CloseableThing> things = ImmutableList.builder();
    try {
        for (File file : files) {
            things.add(readThing(file))
        }
        return things.build();
    } catch (Throwable t) {
        for (CloseableThing thing : things.build()) {
            thing.close();
        }
        throw t;
    }
}

之所以出现代码审查评论,是因为通常有一条规则是不捕获 Throwable。进行这种仅故障清理的旧模式是:

public List<CloseableThing> readThings(List<File> files) throws IOException {
    ImmutableList.Builder<CloseableThing> things = ImmutableList.builder();
    boolean success = false;
    try {
        for (File file : files) {
            things.add(readThing(file))
        }
        success = true;
        return things.build();
    } finally {
        if (!success) {
            for (CloseableThing thing : things.build()) {
                thing.close();
            }
        }
    }
}

我觉得这有点混乱,不完全理解它与捕捉 Throwable 是否有任何不同。在任何一种情况下,异常都会传播。无论哪种情况,当可能发生 OutOfMemoryError 时,都会运行其他代码。

那么终于真的更安全了吗?

【问题讨论】:

  • 我猜他们的理由更符合语义,并且符合放置它的适当位置。即清理意味着在finallycatch 应该处理异常本身。
  • 如果最终代码在OutOfMemoryError 时执行,我认为它会在尝试做某事时再次抛出OutOfMemoryError
  • 你可以不用 finally 在 try (//close here) 块中,因为Java 1.7
  • 如果您需要进行清理,您应该始终使用finally,除非您有充分的理由不这样做。
  • 使用 Java 7 的 try(){} 块的问题是它也会在成功时关闭对象。在这种情况下,如果它们都被成功读取,我特别希望保持打开状态,但是如果异常失败,我想关闭在此之前打开的那些(否则你会泄漏他们持有的任何资源,当错误发生。)

标签: java try-catch finally throwable


【解决方案1】:

ThrowableExceptionError 的父类型,因此捕获 Throwable 意味着同时捕获两个异常作为错误。异常是您可以恢复的东西(例如 IOException),错误是更严重的东西,通常您无法轻松恢复(例如 ClassNotFoundError)所以它不会除非您知道自己在做什么,否则捕获 错误 非常有意义。

【讨论】:

  • 是的,尽管 NullPointerException 并不是一个可以从中恢复的好例子 :-) 这通常是由开发人员/程序员错误引起的。
  • @Keith 同意,让我纠正一下自己,也许是 IOException? ;-)
  • 当然,这是一个更好的例子
  • Error也是某个OutOfMemoryError的父级
  • 确实无法从错误中恢复,但在上面的代码片段中我也没有这样做。我只是清理东西并重新抛出错误,因为我不知道如何处理它。
【解决方案2】:

是否可以捕获 Throwable 以执行清理?

一句话……没有。

问题是,如果你捕获并重新抛出Throwable,你必须声明该方法抛出Throwable ...这将导致任何调用该方法的问题:

  • 调用者现在必须“处理”(可能传播)Throwable
  • 程序员现在无法从编译器获得任何帮助,因为有关检查异常的编译器错误尚未得到处理。 (当您“处理”Throwable 时,这将包括所有尚未处理的已检查和未检查的异常。)

一旦您开始走这条路,throws Throwable 就会像疾病一样通过调用层级传播...


关闭资源的正确方法是使用finally,或者如果您正在编写Java 7 或更高版本,则使用“try with resources”,并使您的资源可自动关闭。

(在您的示例中,这有点棘手,但您可以扩展现有的 List 类以创建“可关闭列表”类,其中 close() 方法关闭所有列表成员。


确实,对于 Java 7 及更高版本,您可以摆脱将封闭方法声明为仅抛出将被捕获的已检查异常。然而,捕捉 Throwable 进行清理并不是人们期望看到的。人们期望看到finally 子句进行清理。如果你用一种时髦的方式来做,你会让你的代码更难阅读……这不是“好的”。即使您的方式更简洁,也不会。

此外,您的版本不会与 Java 6 及更早版本一起编译。


简而言之,我同意你的代码审查者的意见。

我唯一同意的是,如果您的版本和finally 版本都是“安全的”,前提是它们已正确实施。 (问题是程序员必须在你的情况下更加努力地意识到它是安全的......因为你编码它的非惯用方式。)

【讨论】:

  • 我看不出我的例子是如何传播这个问题的。我正在捕获 Throwable,但如您所见,该方法仅抛出 IOException。这在 Java 7 或更高版本中是可能的。
  • 成语论证很有趣。如果我有只应该在失败时发生的清理,那么只在失败情况下运行它对我来说似乎是完全合乎逻辑的......但有趣的是,并不是每个人都这么想。
【解决方案3】:

这是试图回答我自己的问题,但它使用实验和 Java 编译器的结果,所以它并没有特别解决哲学或类似的问题。

这里是一些 catch-cleanup-and-rethrow 的示例代码:

public CompoundResource catchThrowable() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        return new CompoundResource(stream1, stream2);
    } catch (Throwable t) {
        if (stream2 != null) {
            stream2.close();
        }
        if (stream1 != null) {
            stream1.close();
        }
        throw t;
    }
}

编译成以下字节码:

public Exceptions$CompoundResource catchThrowable() throws java.lang.Exception;
  Code:
     0: aconst_null   
     1: astore_1      
     2: aconst_null   
     3: astore_2      
     4: new           #2                  // class java/io/FileInputStream
     7: dup           
     8: ldc           #3                  // String 1
    10: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    13: astore_1      
    14: new           #2                  // class java/io/FileInputStream
    17: dup           
    18: ldc           #5                  // String 2
    20: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    23: astore_2      
    24: new           #6                  // class Exceptions$CompoundResource
    27: dup           
    28: aload_0       
    29: aload_1       
    30: aload_2       
    31: invokespecial #7                  // Method Exceptions$CompoundResource."<init>":(LExceptions;Ljava/io/Closeable;Ljava/io/Closeable;)V
    34: areturn       
    35: astore_3      
    36: aload_2       
    37: ifnull        44
    40: aload_2       
    41: invokevirtual #9                  // Method java/io/InputStream.close:()V
    44: aload_1       
    45: ifnull        52
    48: aload_1       
    49: invokevirtual #9                  // Method java/io/InputStream.close:()V
    52: aload_3       
    53: athrow        
  Exception table:
     from    to  target type
         4    34    35   Class java/lang/Throwable

接下来是一些用于检查失败的代码,在最终和清理中具有相同的语义:

public CompoundResource finallyHack() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    boolean success = false;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        success = true;
        return new CompoundResource(stream1, stream2);
    } finally {
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
    }
}

编译如下:

public Exceptions$CompoundResource finallyHack() throws java.lang.Exception;
  Code:
     0: aconst_null   
     1: astore_1      
     2: aconst_null   
     3: astore_2      
     4: iconst_0      
     5: istore_3      
     6: new           #2                  // class java/io/FileInputStream
     9: dup           
    10: ldc           #3                  // String 1
    12: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    15: astore_1      
    16: new           #2                  // class java/io/FileInputStream
    19: dup           
    20: ldc           #5                  // String 2
    22: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
    25: astore_2      
    26: iconst_1      
    27: istore_3      
    28: new           #6                  // class Exceptions$CompoundResource
    31: dup           
    32: aload_0       
    33: aload_1       
    34: aload_2       
    35: invokespecial #7                  // Method Exceptions$CompoundResource."<init>":(LExceptions;Ljava/io/Closeable;Ljava/io/Closeable;)V
    38: astore        4
    40: iload_3       
    41: ifne          60
    44: aload_2       
    45: ifnull        52
    48: aload_2       
    49: invokevirtual #9                  // Method java/io/InputStream.close:()V
    52: aload_1       
    53: ifnull        60
    56: aload_1       
    57: invokevirtual #9                  // Method java/io/InputStream.close:()V
    60: aload         4
    62: areturn       
    63: astore        5
    65: iload_3       
    66: ifne          85
    69: aload_2       
    70: ifnull        77
    73: aload_2       
    74: invokevirtual #9                  // Method java/io/InputStream.close:()V
    77: aload_1       
    78: ifnull        85
    81: aload_1       
    82: invokevirtual #9                  // Method java/io/InputStream.close:()V
    85: aload         5
    87: athrow        
  Exception table:
     from    to  target type
         6    40    63   any
        63    65    63   any

仔细查看这里发生的情况,它似乎生成了相同的字节码,就好像您在返回点和 catch 块内复制了整个 finally 块一样。换句话说,就好像你写的是这样的:

public CompoundResource finallyHack() throws Exception {
    InputStream stream1 = null;
    InputStream stream2 = null;
    boolean success = false;
    try {
        stream1 = new FileInputStream("1");
        stream2 = new FileInputStream("2");
        success = true;
        CompoundResource result = new CompoundResource(stream1, stream2);
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
        return result;
    } catch (any t) {    // just invented this syntax, this won't compile
        if (!success) {
            if (stream2 != null) {
                stream2.close();
            }
            if (stream1 != null) {
                stream1.close();
            }
        }
        throw t;
    }
}

如果有人真的编写了该代码,你会嘲笑他们。在成功分支中,成功总是正确的,所以有一大块代码永远不会运行,所以你生成的字节码永远不会被执行,只会让你的类文件膨胀。在异常分支中,成功总是错误的,因此您在执行清理之前对值执行了不必要的检查,您知道清理必须发生,这再次增加了类文件的大小。

最需要注意的是:

catch (Throwable)finally 解决方案实际上都捕获了所有异常。

就回答这个问题而言,“可以捕获Throwable 进行清理吗?”...

我仍然不确定,但我知道如果不能为它捕获Throwable,也不能为它使用finally。如果finally 也不行,还剩下什么?

【讨论】:

  • 说真的,您不应该试图通过查看字节码来弄清楚 Java 代码的作用。阅读 JLS。
【解决方案4】:

捕捉Throwablefinally 不可互换。

  • finally 子句中的代码将在退出块时执行 的原因。如果没有抛出异常,它将被执行。因此,它是必须始终执行的清理代码的适当位置。

  • catchThrowable 代码只有在抛出异常时才会被执行。

【讨论】:

  • 虽然我写它的方式,我 finally 中的代码也只会在抛出异常时运行,因为在那种情况下我没有将成功设置为 true。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-16
  • 2015-02-18
  • 1970-01-01
相关资源
最近更新 更多