ry 语句如何更优雅的关闭资源?请看这里!

开发 前端
在 JDK7 之前,在处理必须关闭的资源时,开发人员必须要牢记在try-catch​语句中使用finally执行关闭资源的方法,否则随着程序不断运行,资源泄露将会累计成重大的生产事故,如果你的程序中同时打开了多个资源,你会惊奇的发,关闭资源的代码竟然比业务代码还要多,使得代码更加难以清晰的阅读和管理。

一、摘要

try-with-resources​是 JDK 7 中引入的一个新的异常处理机制,它能让开发人员不用显式的释放try-catch语句块中使用的资源。

比如,我们以文件资源拷贝为示例,大家所熟悉的try-catch-finally写法如下:

public class ResourceTest1 {

public static void main(String[] args) {
BufferedInputStream bin = null;
BufferedOutputStream bout = null;
try {
bin = new BufferedInputStream(new FileInputStream(new File( "test.txt")));
bout = new BufferedOutputStream(new FileOutputStream(new File( "out.txt")));
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭文件流
if (bin != null) {
try {
bin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}

我们现在将其改成使用try-with-resources编程方式,你会惊奇的发现只需要简单的几行代码就可以搞定,不用显式关闭资源,方式如下:

public class ResourceTest2 {

public static void main(String[] args) {
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}

在 JDK7 之前,在处理必须关闭的资源时,开发人员必须要牢记在try-catch​语句中使用finally执行关闭资源的方法,否则随着程序不断运行,资源泄露将会累计成重大的生产事故,如果你的程序中同时打开了多个资源,你会惊奇的发,关闭资源的代码竟然比业务代码还要多,使得代码更加难以清晰的阅读和管理。

因此在这样的背景下,try-with-resources​由此诞生,它的设计初衷就是旨在减轻开发人员释放try块中使用的资源负担。

习惯了try-catch-finally​写法的同学,可能会发出疑问,是不是所有涉及到资源的操作都可以用try-with-resources编程?使用这种编程方式有没有坑?如果有坑,使用的时候哪些地方应该需要注意呢?....

好吧,废话也不多说了,今天我们就一起来看看try-with-resources编程原理。

二、实践解说

try-with-resources​语句能确保每个资源在语句结束时被关闭,但是有一个前提条件,那就是这个资源必须实现了java.lang.AutoCloseable接口,才可以被执行关闭。

try-with-resources​编程模式中,无需开发人员显式关闭资源的前提是,这个资源必须实现java.lang.AutoCloseable​接口,并且重写close​方法,否则无法在try-with-resources中进行声明变量。

下面我们可以关闭单个资源为例,代码如下:

public class TryResourceDemo implements AutoCloseable {

public void doSomething(){
System.out.println("do something");
}

@Override
public void close() throws Exception {
System.out.println("resource is closed");
}
}
public class TryResourceTest {

public static void main(String[] args) {
try(TryResourceDemo res = new TryResourceDemo()) {
res.doSomething();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}

运行结果如下:

do something
resource is closed

可以很清晰的看到,close方法被调用了!

下面我们再打开反编译后的TryResourceTest.class​文件代码,你会惊奇发现,编译器自动给代码加上了finally​方法,并且会调用close方法,将资源关闭!

public class TryResourceTest {

public static void main(String[] args) {
try {
TryResourceDemo res = new TryResourceDemo();
Throwable var2 = null;

try {
res.doSomething();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (res != null) {
if (var2 != null) {
try {
res.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
res.close();
}
}

}
} catch (Exception var14) {
var14.printStackTrace();
}

}
}

也就是说,使用try-with-resources​编程,其实是编译器显式的给代码了添加finally方法,省去开发人员手动关闭资源的操作!

三、资源关闭顺序

上面我们只介绍了关闭单个资源的场景,假如有多个资源时,try-with-resources是如何关闭的呢?

下面还是举例看结果。

public class TryResourceDemo1 implements AutoCloseable {

public void doSomething(){
System.out.println("do something 1");
}

@Override
public void close() throws Exception {
System.out.println("resource 1 is closed");
}
}
public class TryResourceDemo2 implements AutoCloseable {

public void doSomething(){
System.out.println("do something 2");
}

@Override
public void close() throws Exception {
System.out.println("resource 2 is closed");
}
}
public class TryResourceDemoTest {

public static void main(String[] args) {
try(TryResourceDemo1 demo1 = new TryResourceDemo1();
TryResourceDemo2 demo2 = new TryResourceDemo2()) {
System.out.println("do...");
demo1.doSomething();
demo2.doSomething();
} catch(Exception ex) {
ex.printStackTrace();
}
}
}

运行结果如下:

do...
do something 1
do something 2
resource 2 is closed
resource 1 is closed

从结果上可以看出,try语句中越是最后使用的资源,越是最早被关闭。

关于这一点,大家可以从反编译的代码中找到原理!

四、异常处理机制

正常的情况下,try​语句结束时会关闭相关的资源,假如语句内部执行时发生异常,同时我们又显式的调用了finally方法,执行的顺序又是怎样的呢?

下面继续举例看结果。

public class TryThrowResourceDemoTest {

public static void main(String[] args) {
AutoCloseable obj1 = null;
AutoCloseable obj2 = null;
try (TryResourceDemo1 demo1 = new TryResourceDemo1();
TryResourceDemo2 demo2 = new TryResourceDemo2();) {
System.out.println("do...");
obj1 = demo1;
System.out.println(1 / 0);
obj2 = demo2;
System.out.println("over...");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
System.out.println("before finally close");
if (obj1 != null) {
obj1.close();
}
if (obj2 != null) {
obj2.close();
}
System.out.println("after finally close");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

运行结果如下:

do...
resource 2 is closed
resource 1 is closed
before finally close
resource 1 is closed
after finally close
java.lang.ArithmeticException: / by zero
at com.example.java.trywithresources.a.TryThrowResourceDemoTest.main(TryThrowResourceDemoTest.java:18)

可以很清晰的看到,可以得出如下结论:

  • 只要实现了AutoCloseable​接口的类,并且在try​里声明了对象变量,在try​结束后,不管是否发生异常,close方法都会被调用
  • 其次,在try​里越晚声明的对象,会越早被close掉
  • try​结束后自动调用的close​方法,这个动作会早于finally里调用的方法

五、压制异常处理

大部分情况,我们通常不会担心资源的close​会发生异常,现在假设如果try​里声明的资源对象,当执行close方法抛异常时,他们的执行顺序又是怎样的呢?我们又如何获取这种异常呢?

还是眼见为实,下面以举例看结果。

public class TryThrowableResourceDemo1 implements AutoCloseable {

public void doSomething(){
System.out.println("do something 1");
throw new NullPointerException("TryThrowableResourceDemo1: doSomething() NullPointerException");
}

@Override
public void close() throws Exception {
System.out.println("TryThrowableResourceDemo1 is closed");
throw new NullPointerException("TryThrowableResourceDemo1: close() NullPointerException");
}
}
public class TryThrowableResourceDemo2 implements AutoCloseable {

public void doSomething(){
System.out.println("do something 2");
throw new NullPointerException("TryThrowableResourceDemo2: doSomething() NullPointerException");
}

@Override
public void close() throws Exception {
System.out.println("TryThrowableResourceDemo2 is closed");
throw new NullPointerException("TryThrowableResourceDemo2: close() NullPointerException");
}
}
public class TryThrowableResourceDemoTest {

public static void main(String[] args) {
try (TryThrowableResourceDemo1 demo1 = new TryThrowableResourceDemo1();
TryThrowableResourceDemo2 demo2 = new TryThrowableResourceDemo2()) {
System.out.println("do...");
demo1.doSomething();
demo2.doSomething();
} catch (Exception e) {
System.out.println("gobal: exception");
System.out.println(e.getMessage());
Throwable[] suppressed = e.getSuppressed();
for (int i = 0; i < suppressed.length; i++){
System.out.println(suppressed[i].getMessage());
}
}
}
}

运行结果如下:

do...
do something 1
TryThrowableResourceDemo2 is closed
TryThrowableResourceDemo1 is closed
gobal: exception
TryThrowableResourceDemo1: doSomething() NullPointerException
TryThrowableResourceDemo2: close() NullPointerException
TryThrowableResourceDemo1: close() NullPointerException

从运行结果我们可以很清晰的看到,对于try​语句块内的异常,我们可以通过e.getMessage()​获取,对于close()​方法抛出的异常,其实编译器对这部分的异常进行特殊处理,将其放入到集合数组中了,因此我们需要通过e.getSuppressed()方法来获取。

具体反编译后的代码如下:

public class TryThrowableResourceDemoTest {

public static void main(String[] args) {
try {
TryThrowableResourceDemo1 demo1 = new TryThrowableResourceDemo1();
Throwable var34 = null;

try {
TryThrowableResourceDemo2 demo2 = new TryThrowableResourceDemo2();
Throwable var4 = null;

try {
System.out.println("do...");
demo1.doSomething();
demo2.doSomething();
} catch (Throwable var29) {
var4 = var29;
throw var29;
} finally {
if (demo2 != null) {
if (var4 != null) {
try {
demo2.close();
} catch (Throwable var28) {
var4.addSuppressed(var28);
}
} else {
demo2.close();
}
}

}
} catch (Throwable var31) {
var34 = var31;
throw var31;
} finally {
if (demo1 != null) {
if (var34 != null) {
try {
demo1.close();
} catch (Throwable var27) {
var34.addSuppressed(var27);
}
} else {
demo1.close();
}
}

}
} catch (Exception var33) {
System.out.println("gobal: exception");
System.out.println(var33.getMessage());
Throwable[] suppressed = var33.getSuppressed();

for(int i = 0; i < suppressed.length; ++i) {
System.out.println(suppressed[i].getMessage());
}
}

}
}

六、关闭资源的坑

在实际的使用中,不管是使用try-with-resource​编程还是使用try-catch-finally​编程,一定需要了解资源的close方法内部的实现逻辑,否则还是可能会导致资源泄露。

举个例子,在 Java BIO 中采用了大量的装饰器模式。当调用装饰器的 close 方法时,本质上是调用了装饰器包装的流对象的 close 方法。比如:

public class TryWithResource {
public static void main(String[] args) {
try (FileInputStream fin = new FileInputStream(new File("input.txt"));
GZIPOutputStream out = new GZIPOutputStream(new FileOutputStream(new File("out.txt")))) {
byte[] buffer = new byte[4096];
int read;
while ((read = fin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}

在上述代码中,我们从FileInputStream​中读取字节,并且写入到GZIPOutputStream​中。GZIPOutputStream​实际上是FileOutputStream的装饰器。

由于try-with-resource​的特性,实际编译之后的代码会在后面带上finally​代码块,并且在里面调用fin.close()​方法和out.close()方法。

我们再来看GZIPOutputStream​类的close方法。

public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}

在调用out​变量的close​方法之前,GZIPOutputStream​还做了finish​操作,该操作还会继续往FileOutputStream​中写压缩信息,此时如果出现异常,则out.close()​方法会被略过,而out​变量实际上代表的是被装饰的FileOutputStream类,这个才是最底层的资源关闭方法。

正确的做法应该是在try-with-resource​中单独声明最底层的资源,保证对应的close​方法一定能够被调用。在刚才的例子中,我们需要单独声明每个FileInputStream​以及FileOutputStream,改成如下方式:

public class TryWithResource {
public static void main(String[] args) {
try (FileInputStream fin = new FileInputStream(new File("input.txt"));
FileOutputStream fout = new FileOutputStream(new File("out.txt"));
GZIPOutputStream out = new GZIPOutputStream(fout)) {
byte[] buffer = new byte[4096];
int read;
while ((read = fin.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
}
catch (IOException e) {
e.printStackTrace();
}
}
}

编译器会自动生成fout.close()的代码,这样肯定能够保证真正的流被关闭。

七、小结

在处理必须关闭的资源时,使用try-with-resources​语句替代try-catch-finally语句,你会惊奇的发现,编写的代码更简洁,更清晰,同时也省去了手动显式释放资源的烦恼。

因此在实际编程过程中,推荐大家采用这种方式编写,同时要关注close方法内部的实现逻辑,避免资源泄露,服务宕机!

八、参考

1、知乎 - 深入理解Java try-with-resource

2、csdn - try - with - resources详解

责任编辑:武晓燕 来源: Java极客技术
相关推荐

2018-07-12 14:20:33

SQLSQL查询编写

2021-05-13 09:35:02

Google桌面网页版搜索界面

2023-12-21 10:26:30

​​Prettier

2022-05-13 08:48:50

React组件TypeScrip

2022-03-11 12:14:43

CSS代码前端

2021-01-19 10:35:49

JVM场景函数

2022-04-11 08:17:07

JVMJava进程

2021-03-28 09:17:18

JVM场景钩子函数

2021-12-13 14:37:37

React组件前端

2021-12-07 08:16:34

React 前端 组件

2017-12-19 10:03:44

JavaLinux代码

2021-01-20 08:26:16

中间件技术spring

2022-07-03 10:23:06

机器人场景个性化

2021-06-25 15:53:25

Kubernetes程序技巧

2021-12-29 17:24:16

Kubernetes集群事件

2019-09-29 16:17:25

Java代码性能编程语言

2019-05-05 15:50:39

VSCode编辑器程序员

2024-02-23 08:57:42

Python设计模式编程语言

2023-10-13 07:36:58

Java函数式编程

2022-06-28 08:01:26

hook状态管理state
点赞
收藏

51CTO技术栈公众号