【问题标题】:Try-with-resources and return statements in javajava中的try-with-resources和return语句
【发布时间】:2014-05-21 18:45:58
【问题描述】:

我想知道是否将 return 语句放在 try-with-resources 块中会阻止资源自动关闭。

try(Connection conn = ...) {
    return conn.createStatement().execute("...");
}

如果我写这样的东西,Connection 会被关闭吗?在 Oracle 文档中指出:

try-with-resources 语句确保每个资源在语句结束时关闭。

如果由于 return 语句而从未到达语句末尾会发生什么?

【问题讨论】:

  • 如果方法堆栈弹出,则语句结束。你认为抛出异常时会发生什么?
  • return 流经finally 子句的主体,这是资源被释放的地方。

标签: java return try-with-resources


【解决方案1】:

基于Oracle's tutorial,“无论try 语句是正常完成还是突然完成,[资源] 都将被关闭”。它将abruptly 定义为异常。

try 内返回是一个突然完成的例子,由JLS 14.1 定义。

【讨论】:

  • 这在技术上是不正确的。 return 语句未正常完成,因此 try 块未正常完成。它突然完成。
  • @SotiriosDelimanolis,我一直认为这是正常的,但我找不到任何文档。你能找到这方面的任何文件吗?
  • 我在打电话 :(。如果您在包含 try catch 块的章节中转到 JLS,还有一节定义了正常完成的含义。查找返回语句中的条目.
  • @SotiriosDelimanolis,我找到了它并更新了我的答案。谢谢指正!
【解决方案2】:

资源将自动关闭(即使使用return 语句),因为它实现了AutoCloseable 接口。这是一个输出“已成功关闭”的示例:

public class Main {

    public static void main(String[] args) {
        try (Foobar foobar = new Foobar()) {
            return;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class Foobar implements AutoCloseable {

    @Override
    public void close() throws Exception {
        System.out.println("closed successfully");
    }
}

【讨论】:

  • 那么,当您返回foobar 时会发生什么?然后你会返回一个已经关闭的 Foobar 实例吗?
  • 是的,我会说对象必须在返回之前关闭
  • 我刚刚对此进行了测试,是的,首先调用close(),然后您将获得一个封闭对象作为返回值。
【解决方案3】:

AutoCloseable 接口乍一看会使代码的执行顺序混乱。让我们用一个例子来看看:

public class Main {

    // An expensive resource which requires opening / closing
    private static class Resource implements AutoCloseable {

        public Resource() {
            System.out.println("open");
        }
        
        @Override public void close() throws Exception {
            System.out.println("close");
        }
    }
    
    // find me a number!
    private static int findNumber() {
        // open the resource
        try(Resource resource = new Resource()) {
            // do some business logic (usually involving the resource) and return answer
            return 2 + 2;
        } catch(Exception e) {
            // resource encountered a problem
            throw new IllegalStateException(e);
        }
    }
    
    public static void main(String[] args) {
        System.out.println(findNumber());
    }
}

上面的代码尝试打开一些Resource 并使用资源执行一些业务逻辑(在这种情况下只是一些算术)。运行代码将打印:

open
close
4

因此,Resource 在退出 try-with-resource 块之前已关闭。为了弄清楚到底发生了什么,让我们重新组织findNumber() 方法。

    private static int findNumber() {
        // open the resource
        int number;
        try(Resource resource = new Resource()) {
            // do some business logic and return answer
            number = 2 + 2;
        } catch(Exception e) {
            // resource encountered a problem
            throw new IllegalStateException(e);
        }
        return number;
    }

从概念上讲,这就是将return 放置在try-with-resource 块中时发生的事情。 return 操作移到 try-with-resource 块之后,以允许 AutoCloseable 对象在返回之前关闭。

因此我们可以得出结论,try-with-resource 块中的 return 操作只是语法糖,您不必担心在 AutoCloseable 关闭之前返回。

【讨论】:

    【解决方案4】:

    好的答案已经发布。我只是采取了一种不同的方法,因为这感觉像是一个深入了解一些可能有一天会派上用场的细节的机会,它试图通过阅读一些字节码来回答这个问题。

    有几个场景 - 看看

    • try 块中的异常
    • 在退出 try 块期间关闭 auto-closeable 时出现异常
    • 在处理早期异常期间closing auto-closeable 资源时出现异常
    • 在 try 块中返回,在返回之前执行 close

    第一个场景通常是在 java 中使用 try-with 时的首要考虑。我们可以通过查看字节码来尝试理解其他三种情况。最后一个场景解决了您的问题。

    分解下面main 方法的字节码

    import java.io.*;
    
    class TryWith {
    
      public static void main(String[] args) {
        try(PrintStream ps = System.out) {
           ps.println("Hey Hey");
           return;
        }
      }
    }
    

    让我们分小部分回顾一下(省略了一些细节)

        Code:
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: astore_1
    

    0:获取静态字段System.out
    3:将字段存储到插槽1的LocalVariableTable(lvt)中。

    查看lvt我们可以确认第一个插槽是java.io.PrintStream,它的名字是ps

          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                4      35     1    ps   Ljava/io/PrintStream;
                0      39     0  args   [Ljava/lang/String;
    
             4: aload_1
             5: ldc           #3                  // String Hey Hey
             7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    

    4:加载ps (aload_1)
    5:从常量池中加载常量(ldc)、hey hey
    7:调用打印行方法,这会消耗操作数堆栈中的pshey hey

            10: aload_1
            11: ifnull        18
            14: aload_1
            15: invokevirtual #5                  // Method java/io/PrintStream.close:()V
            18: return
    

    10 - 11:将ps 加载到操作数堆栈上。检查ps 是否为null,如果为null,则从函数中跳转到18return
    14 - 18:加载ps,调用closereturn

    上面的内容特别有趣,因为它表明如果Auto-Closeable 资源是null 而不是throw 异常,则try-with 块将起作用。当然,即使它确实有效,也没有实际意义——除非在try 块中没有访问资源。任何访问都会导致 NPE。

    以上也是正常流程,遇到异常怎么办?我们来看看异常表

          Exception table:
             from    to  target type
                 4    10    19   Class java/lang/Throwable
                24    28    31   Class java/lang/Throwable
    

    这告诉我们,字节码 4-10 之间的任何 java.lang.Throwable 类型的异常都在目标 19 处处理。对于第 24-28 行的第 31 行也是如此。

            19: astore_2
            20: aload_1
            21: ifnull        37
            24: aload_1
            25: invokevirtual #5                  // Method java/io/PrintStream.close:()V
            28: goto          37
    

    19:将异常存储到局部变量2
    20 - 25:这与我们之前看到的相同模式 close 仅在 ps 不是 null 时调用 28:跳转指令到37

            37: aload_2
            38: athrow
    

    37:加载存储在局部变量表中位置2的对象,之前我们将异常存储在这个位置。
    38:抛出异常

    但是,当 close 由于早期的异常而执行时,在 close 期间发生异常的情况如何。让我们回顾一下异常表

          Exception table:
             from    to  target type
                 4    10    19   Class java/lang/Throwable
                24    28    31   Class java/lang/Throwable
    

    也就是异常表的第二行,我们看看target 31对应的字节码

            31: astore_3
            32: aload_2
            33: aload_3
            34: invokevirtual #7                  // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
            37: aload_2
            38: athrow
    

    31:次要异常存储在插槽 3 的局部变量中。
    32:从插槽 3 重新加载原始异常。
    33-34:将次要异常添加为原始异常的抑制异常。
    37-38:抛出新异常,我们之前已经介绍过这些行。

    重新审视我们在开头列出的考虑

    • 在退出try 块期间关闭auto-closeable 时出现异常。
      ** 引发异常,try 块退出 abruptly
    • closingauto-closeable 资源在处理早期异常时出现异常。
      ** 将抑制的异常添加到原始异常并抛出原始异常。 try 块退出 abruptly
    • 在 try 块中返回,在返回之前关闭执行。
      ** close 在 try 块中的 return 之前执行

    重温我们在字节码中遇到的auto-closeable资源为null的有趣场景,我们可以用

    import java.io.*;
    
    class TryWithAnother {
    
      public static void main(String[] args) {
        try(PrintStream ps = null) {
           System.out.println("Hey Hey");
           return;
        }
      }
    }
    

    毫不奇怪,我们在控制台上得到了Hey Hey 的输出,也不例外。

    最后但很重要的一点是,这个字节码是 JLS 的兼容实现。这种方法非常方便确定您的实际执行需要什么,可能还有其他兼容的替代方案 - 在这种情况下,我想不出任何办法。但是考虑到这一点,如果不指定我的javac 版本,此响应将不完整

    openjdk 11.0.9.1 2020-11-04
    OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.9.1+1)
    OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.9.1+1, mixed mode)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-18
      • 2012-03-04
      • 2015-04-26
      • 2013-07-21
      • 1970-01-01
      相关资源
      最近更新 更多