关于try-with-resources不知道的事
谁来背锅
try-with-resource是Java 1.7中新增的,来打开资源,而无需手动关闭语法糖。官方介绍如下:
The try-with-resources statement is a try statement that declares one or more resources.
A resource is an object that must be closed after the program is finished with it.
The try-with-resources statement ensures that each resource is closed at the end of the statement.
Any object that implements java.lang.AutoCloseable, which includes all objects which implement java.io.Closeable, can be used as a resource.
还十分贴心的贴上了说明代码
The following example reads the first line from a file. It uses an instance of BufferedReader to read data from the file. BufferedReader is a resource that must be closed after the program is finished with it:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
In this example, the resource declared in the try-with-resources statement is a BufferedReader. The declaration statement appears within parentheses immediately after the try keyword. The class BufferedReader, in Java SE 7 and later, implements the interface java.lang.AutoCloseable. Because the BufferedReader instance is declared in a try-with-resource statement, it will be closed regardless of whether the try statement completes normally or abruptly (as a result of the method BufferedReader.readLine throwing an IOException).
Prior to Java SE 7, you can use a finally block to ensure that a resource is closed regardless of whether the try statement completes normally or abruptly. The following example uses a finally block instead of a try-with-resources statement:
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
尤其是最后那句
The following example uses a finally block instead of a try-with-resources statement
让我误以为这两条代码是完全等价的。
由soot而来的疑惑
一个简单的例子
import com.google.common.flogger.FluentLogger;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FinalTest {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
public static void finalTest(String path, String[] contant) {
File outFile = new File(path);
try (FileOutputStream os = new FileOutputStream(outFile)) {
for (String s : contant) {
os.write(s.getBytes());
}
} catch (IOException e) {
logger.atSevere().withCause(e).log();
} finally {
System.out.println("Out file: " + path);
}
}
}
按照文档中的例子,close函数等价于在finally中执行,也就是说在exception block执行完成之后
FileOutputStream os = new FileOutputStream(outFile)
try {
...
} catch (IOException e) {
...
} finally {
os.close(); // 这里执行
System.out.println("Out file: " + path);
os.close(); // 或者这里执行
}
然而它生成的jimple
public static void finalTest(java.lang.String, java.lang.String[])
{
...
...
label08:
if $r8 == null goto label13;
if r32 == null goto label12;
label09:
virtualinvoke $r8.<java.io.FileOutputStream: void close()>();
label10:
goto label13;
label11:
$r23 := @caughtexception;
virtualinvoke r32.<java.lang.Throwable: void addSuppressed(java.lang.Throwable)>($r23);
goto label13;
label12:
virtualinvoke $r8.<java.io.FileOutputStream: void close()>();
label13:
throw $r21;
...
...
label15: // exception label
$r10 := @caughtexception;
$r11 = <com.sbrella.test.FinalTest: com.google.common.flogger.FluentLogger logger>;
$r12 = virtualinvoke $r11.<com.google.common.flogger.FluentLogger: com.google.common.flogger.LoggingApi atSevere()>();
$r13 = (com.google.common.flogger.FluentLogger$Api) $r12;
$r14 = interfaceinvoke $r13.<com.google.common.flogger.FluentLogger$Api: com.google.common.flogger.LoggingApi withCause(java.lang.Throwable)>($r10);
$r15 = (com.google.common.flogger.FluentLogger$Api) $r14;
interfaceinvoke $r15.<com.google.common.flogger.FluentLogger$Api: void log()>();
label16:
$r17 = <java.lang.System: java.io.PrintStream out>;
$r16 = new java.lang.StringBuilder;
specialinvoke $r16.<java.lang.StringBuilder: void <init>()>();
$r18 = virtualinvoke $r16.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>("Out file: ");
$r19 = virtualinvoke $r18.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r0);
$r20 = virtualinvoke $r19.<java.lang.StringBuilder: java.lang.String toString()>();
virtualinvoke $r17.<java.io.PrintStream: void println(java.lang.String)>($r20);
goto label19;
label17:
$r25 := @caughtexception;
label18:
$r27 = <java.lang.System: java.io.PrintStream out>;
$r26 = new java.lang.StringBuilder;
specialinvoke $r26.<java.lang.StringBuilder: void <init>()>();
$r28 = virtualinvoke $r26.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>("Out file: ");
$r29 = virtualinvoke $r28.<java.lang.StringBuilder: java.lang.StringBuilder append(java.lang.String)>(r0);
$r30 = virtualinvoke $r29.<java.lang.StringBuilder: java.lang.String toString()>();
virtualinvoke $r27.<java.io.PrintStream: void println(java.lang.String)>($r30);
throw $r25;
label19:
return;
}
只有一个exception label,且没有任何跳转。也就是说在exception执行完之后直接执行finally中的println,然后便退出了。
那close呢,在exception之前就执行了。
Bytecode的答案
要知道真正的执行顺序自然是到class文件中寻找答案,直接看bytecode
public static void finalTest(java.lang.String, java.lang.String[]);
descriptor: (Ljava/lang/String;[Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=12, args_size=2
...
...
49: invokevirtual #9 // Method java/lang/String.getBytes:()[B
52: invokevirtual #10 // write 函数触发异常 java/io/FileOutputStream.write:([B)V
55: iinc 7, 1
58: goto 32
61: aload_3
62: ifnull 142
65: aload 4
67: ifnull 89
70: aload_3
71: invokevirtual #11 // Method java/io/FileOutputStream.close:()V
74: goto 142
77: astore 5
79: aload 4
81: aload 5
83: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
86: goto 142
89: aload_3
90: invokevirtual #11 // Method java/io/FileOutputStream.close:()V
93: goto 142
96: astore 5 // 根据exception table,write的异常跳转到这里
98: aload 5
100: astore 4
102: aload 5
104: athrow // throw 一个异常在并跳转到105
105: astore 9
107: aload_3 // 取出FileOutputStream
108: ifnull 139
111: aload 4 // 4中保存的是Exception变量
113: ifnull 135
116: aload_3
117: invokevirtual #11 // Method java/io/FileOutputStream.close:()V
120: goto 139
123: astore 10
125: aload 4
127: aload 10
129: invokevirtual #13 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V
132: goto 139
135: aload_3
136: invokevirtual #11 // Method java/io/FileOutputStream.close:()V
139: aload 9
141: athrow // 最终在这里再次throw
142: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
145: new #15 // class java/lang/StringBuilder
148: dup
149: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
152: ldc #17 // String Out file:
154: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
157: aload_0
158: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
161: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
164: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
167: goto 252
170: astore_3 // 由于是IOException,在这里被捕获
171: getstatic #22 // Field logger:Lcom/google/common/flogger/FluentLogger;
174: invokevirtual #23 // Method com/google/common/flogger/FluentLogger.atSevere:()Lcom/google/common/flogger/LoggingApi;
177: checkcast #24 // class com/google/common/flogger/FluentLogger$Api
180: aload_3
181: invokeinterface #25, 2 // InterfaceMethod com/google/common/flogger/FluentLogger$Api.withCause:(Ljava/lang/Throwable;)Lcom/google/common/flogger/LoggingApi;
186: checkcast #24 // class com/google/common/flogger/FluentLogger$Api
189: invokeinterface #26, 1 // InterfaceMethod com/google/common/flogger/FluentLogger$Api.log:()V
194: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
197: new #15 // class java/lang/StringBuilder
200: dup
201: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
204: ldc #17 // String Out file:
206: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
209: aload_0
210: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
213: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
216: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
219: goto 252
222: astore 11
224: getstatic #14 // Field java/lang/System.out:Ljava/io/PrintStream;
227: new #15 // class java/lang/StringBuilder
230: dup
231: invokespecial #16 // Method java/lang/StringBuilder."<init>":()V
234: ldc #17 // String Out file:
236: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
239: aload_0
240: invokevirtual #18 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
243: invokevirtual #19 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
246: invokevirtual #20 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
249: aload 11
251: athrow
252: return
Exception table:
from to target type
70 74 77 Class java/lang/Throwable
21 61 96 Class java/lang/Throwable
21 61 105 any
116 120 123 Class java/lang/Throwable
96 107 105 any
9 142 170 Class java/io/IOException
9 142 222 any
170 194 222 any
222 224 222 any
直接假设write函数触发IOException,模拟执行一下。
从Exception Table中看到,write函数的异常会去往96的astore指令,然后在104通过指令athrow触发异常并去往105。局部变量表中第3个保存的就是FileOutputStream的this指针,通过aload_3获取并判断是否为null,4中保存的是catch到的Exception,是否为null。如果this指针不为空则调用close函数。之后会在141重新throw,由于该Exception是IOException,所以会被170处捕获,也就是进入了Java源代码中的exception block。
根据上述的执行流程,更接近于以下的Java代码:
public static void finalTest(String path, String[] contant) {
File outFile = new File(path);
try {
FileOutputStream os = new FileOutputStream(outFile);
try {
for (String s : contant) {
os.write(s.getBytes());
}
} catch(Throwable e) {
if (os != null) {
os.close();
}
throw e;
}
} catch (IOException e) {
logger.atSevere().withCause(e).log();
} finally {
System.out.println("Out file: " + path);
}
}
注意os.close()本身也是会throw IOException。根据Exception Table,bytecode中117处的那次close调用如果触发异常则会跳转到123,调用addSuppressed进行追加。
针对这种情况可以做一个简单的验证
public class Resource implements AutoCloseable {
@Override
public void close() {
throw new RuntimeException("Resource");
}
}
public class Main {
public static void main(String[] args) {
try (Resource r = new Resource()) {
throw new RuntimeException("main");
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行后打印的结果
java.lang.RuntimeException: main
at Main.main(Main.java:4)
Suppressed: java.lang.RuntimeException: Resource
at Resource.close(Resource.java:4)
at Main.main(Main.java:5)
文档链接
https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!