Java 中的嵌套 try-finally 块、等价和保证

Nested try-finally blocks, equivalence and guarantees in Java

提问人:stonar96 提问时间:11/8/2023 最后编辑:stonar96 更新时间:11/9/2023 访问量:86

问:

最近我正在研究一种“清理”方法,我想确保尝试执行每个语句。我的第一种方法是这样的(语句在哪里):s#;

try {
    s1;
} finally {
    try {
        s2;
    } finally {
        s3;
    }
}

为简单起见,我在这里省略了异常处理部分(块),我在其中存储了第一个出现和随后被抑制的 s,以便稍后重新抛出。catchThrowable

然后我注意到还有另一种(也许更正确?)方法可以做同样的事情:

try {
    try {
        s1;
    } finally {
        s2;
    }
} finally {
    s3;
}

在我看来,第二种方法感觉更正确,因为一旦尝试,我们已经在两个区块内,这保证了所有区块都将被执行。s1;tryfinally

我的问题是:就 JVM 提供的保证而言,这些方法之间实际上存在差异吗?

此外,从理论上讲,JVM 是否有可能在第一个代码示例中抛出 an(参见下面的代码),以便不再尝试内部 - 块?Error// not a statementtryfinally

try {
    s1;
} finally {
    // not a statement
    try {
        s2;
    } finally {
        s3;
    }
}

根据 11.1.3。异步异常,异常基本上可以在任何地方抛出。这就是为什么我认为嵌套 - 块是必要的的主要原因,如第二个代码示例所示。还是我理解了 JLS 错误,只有在也有语句的情况下才会发生异常?tryfinally

Java 错误处理 嵌套的 try-catch-finally try-finally

评论

0赞 tgdavies 11/8/2023
查看生成的字节码可能有助于回答您的问题,以及语言规范。
0赞 user207421 11/8/2023
这不是“全有或全无”。这就是“全部”。

答:

1赞 rzwitserloot 11/8/2023 #1

将 an 和 an 添加到组合中,它变得比现在更清楚,这是一个难以维护的混乱,需要注释解释为什么你有这种奇怪的嵌套尝试块组合。s4s5

因此,我建议你这样做:

Throwable t = null;
try { s1(); } catch (Throwable e) {t = e;}
try { s2(); } catch (Throwable e) {t = e;}
try { s3(); } catch (Throwable e) {t = e;}
if (t != null) throw t;

您可以花一些时间,并用实用程序方法()替换,如果已经发生异常,则将它们附加为抑制异常,就像try-finally一样。t = e;t = update(t, e);

或者,更好的是,添加 lambda 支持:

public final class Cleanup {
  public interface CleanupJob<T extends Throwable> {
    void cleanup() throws T;
  }

  @SafeVarargs
  public static <T extends Throwable> void cleanup(CleanupJob<? extends T>... jobs) {
    Throwable ex = null;
    for (var job : jobs) try {
      job.cleanup();
    } catch (Exception e) {
      if (ex == null) {
        ex = e;
      } else {
        ex.addSuppressed(e);
      }
    }

    if (ex != null) throw (T) ex;
  }
}

要使用:

import static CleanupRunner.cleanup;

...

cleanup(
() -> s1(),
() -> s2(),
() -> s3(),
() -> s4());

// or even

cleanup(
  this::s1,
  ref::s2,
  SomeClass::staticS3
);

此外,从理论上讲,JVM 在第一个代码示例中抛出 Error 是否可能 // 而不是语句(请参阅下面的代码),从而不再尝试内部 try 块?

事实并非如此。

评论

0赞 stonar96 11/9/2023
“不是。”你有什么证据证明这一点吗(例如来自JLS)?在我看来,JLS似乎说,例外情况基本上无处不在。参见 JLS 11.1.3。异步异常:docs.oracle.com/javase/specs/jls/se8/html/...。这正是我问这个问题的原因。否则我不会嵌套 - 块,但我会做与您的第一个代码示例相同的事情。tryfinally
0赞 rzwitserloot 11/9/2023
你要求我证明是否定的。这是不可能的。
0赞 Reilas 11/8/2023 #2

"...我想确保每个语句都尝试执行(全部或根本不执行)。..."

在前面,使用提供的示例,将执行所有语句。
不会有,“......或者根本没有”。

使用布尔值,这是一个示例。

boolean exit = false;
try {
        
} catch (Throwable t) {
    exit = true;
} finally {
    if (!exit) {
        try {

        } catch (Throwable t) {
            exit = true;
        } finally {
        
        }
    }
}

通常,最终代码块用于简单地应用最终过程。
在某些情况下,这实际上是必要的,例如,关闭 Closeable 对象。
实际上,这就是实现 AutoCloseable 接口和 try-with-resources 语法的原因。

int value;
try {
    value = Integer.parseInt("abc");
} finally {
    value = 123;
}

换句话说,虽然您的代码是允许的并且有效,但您通常不希望继续嵌套 try 代码块。
只需重新评估您当前的设计和控制流程即可。

"...然后我注意到还有另一种(也许更正确?)方法可以做同样的事情......”

没错,此示例将提供相同的功能。
而且,我想解释器会为每个编译一个类似的字节码”。

"...就 JVM 提供的保证而言,这些方法之间实际上存在差异吗?..."

保证是一样的;抽象是一样的。
每个最终方块都会被执行,无论是否投掷了 Throwable;与 Catch 块不同。

"...从理论上讲,JVM是否有可能在第一个代码示例中抛出错误...这样内部的尝试块就不再尝试了?..."

同样,如果在该行抛出 Error,则不会执行任何后续代码。
该程序将简单地停止并退出。
您还需要将该代码放在 try-catch 块中。

try {

} finally {
    try {
        throw new Error();
    } finally {

    }
}

评论

0赞 stonar96 11/9/2023
对不起,我在你的回答中并没有真正找到我实际问题的答案。我已经编辑了我的问题,以使我的实际观点更清晰一些,并解释为什么我认为嵌套是必要的。“你还需要将该代码放在try-catch块中。关键是,这个地方没有代码,问题是,JVM还能在这个地方抛出错误吗?如果是这样,这将与你的回答相矛盾,即我的两个例子是等价的。另外,我知道当抛出异常时会发生什么。那不是我的问题。
0赞 Reilas 11/9/2023
@stonar96,Throwable 需要源自代码中的某些内容。