try-with-resources
是 JDK 7 中引入的一个新的异常处理机制,它能让开发人员不用显式的释放try-catch
语句块中使用的资源。
比如,我们以文件资源拷贝为示例,大家所熟悉的try-catch-finally
写法如下:
publicclassResourceTest1{ publicstaticvoidmain(String[]args){ BufferedInputStreambin =null;BufferedOutputStreambout =null;try{ bin =newBufferedInputStream(newFileInputStream(newFile("test.txt")));bout =newBufferedOutputStream(newFileOutputStream(newFile("out.txt")));intb;while((b =bin.read())!=-1){ bout.write(b);}}catch(IOExceptione){ e.printStackTrace();}finally{ //关闭文件流if(bin !=null){ try{ bin.close();}catch(IOExceptione){ e.printStackTrace();}}if(bout !=null){ try{ bout.close();}catch(IOExceptione){ e.printStackTrace();}}}}}
我们现在将其改成使用try-with-resources
编程方式,你会惊奇的发现只需要简单的几行代码就可以搞定,不用显式关闭资源,方式如下:
publicclassResourceTest2{ publicstaticvoidmain(String[]args){ try(BufferedInputStreambin =newBufferedInputStream(newFileInputStream(newFile("test.txt")));BufferedOutputStreambout =newBufferedOutputStream(newFileOutputStream(newFile("out.txt")))){ intb;while((b =bin.read())!=-1){ bout.write(b);}}catch(IOExceptione){ 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
中进行声明变量。
下面我们可以关闭单个资源为例,代码如下:
publicclassTryResourceDemoimplementsAutoCloseable{ publicvoiddoSomething(){ System.out.println("do something");}@Overridepublicvoidclose()throwsException{ System.out.println("resource is closed");}}
publicclassTryResourceTest{ publicstaticvoidmain(String[]args){ try(TryResourceDemores =newTryResourceDemo()){ res.doSomething();}catch(Exceptionex){ ex.printStackTrace();}}}
运行结果如下:
do somethingresource is closed
可以很清晰的看到,close
方法被调用了!
下面我们再打开反编译后的TryResourceTest.class
文件代码,你会惊奇发现,编译器自动给代码加上了finally
方法,并且会调用close
方法,将资源关闭!
publicclassTryResourceTest{ publicstaticvoidmain(String[]args){ try{ TryResourceDemores =newTryResourceDemo();Throwablevar2 =null;try{ res.doSomething();}catch(Throwablevar12){ var2 =var12;throwvar12;}finally{ if(res !=null){ if(var2 !=null){ try{ res.close();}catch(Throwablevar11){ var2.addSuppressed(var11);}}else{ res.close();}}}}catch(Exceptionvar14){ var14.printStackTrace();}}}
也就是说,使用try-with-resources
编程,其实是编译器显式的给代码了添加finally
方法,省去开发人员手动关闭资源的操作!
上面我们只介绍了关闭单个资源的场景,假如有多个资源时,try-with-resources
是如何关闭的呢?
下面还是举例看结果。
publicclassTryResourceDemo1implementsAutoCloseable{ publicvoiddoSomething(){ System.out.println("do something 1");}@Overridepublicvoidclose()throwsException{ System.out.println("resource 1 is closed");}}
publicclassTryResourceDemo2implementsAutoCloseable{ publicvoiddoSomething(){ System.out.println("do something 2");}@Overridepublicvoidclose()throwsException{ System.out.println("resource 2 is closed");}}
publicclassTryResourceDemoTest{ publicstaticvoidmain(String[]args){ try(TryResourceDemo1demo1 =newTryResourceDemo1();TryResourceDemo2demo2 =newTryResourceDemo2()){ System.out.println("do...");demo1.doSomething();demo2.doSomething();}catch(Exceptionex){ ex.printStackTrace();}}}
运行结果如下:
do...do something 1do something 2resource 2 is closedresource 1 is closed
从结果上可以看出,try
语句中越是最后使用的资源,越是最早被关闭。
关于这一点,大家可以从反编译的代码中找到原理!
正常的情况下,try
语句结束时会关闭相关的资源,假如语句内部执行时发生异常,同时我们又显式的调用了finally
方法,执行的顺序又是怎样的呢?
下面继续举例看结果。
publicclassTryThrowResourceDemoTest{ publicstaticvoidmain(String[]args){ AutoCloseableobj1 =null;AutoCloseableobj2 =null;try(TryResourceDemo1demo1 =newTryResourceDemo1();TryResourceDemo2demo2 =newTryResourceDemo2();){ System.out.println("do...");obj1 =demo1;System.out.println(1/0);obj2 =demo2;System.out.println("over...");}catch(Exceptione){ 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(Exceptione){ e.printStackTrace();}}}}
运行结果如下:
do...resource 2 is closedresource 1 is closedbefore finally closeresource 1 is closedafter finally closejava.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
方法抛异常时,他们的执行顺序又是怎样的呢?我们又如何获取这种异常呢?
还是眼见为实,下面以举例看结果。
publicclassTryThrowableResourceDemo1implementsAutoCloseable{ publicvoiddoSomething(){ System.out.println("do something 1");thrownewNullPointerException("TryThrowableResourceDemo1: doSomething() NullPointerException");}@Overridepublicvoidclose()throwsException{ System.out.println("TryThrowableResourceDemo1 is closed");thrownewNullPointerException("TryThrowableResourceDemo1: close() NullPointerException");}}
publicclassTryThrowableResourceDemo2implementsAutoCloseable{ publicvoiddoSomething(){ System.out.println("do something 2");thrownewNullPointerException("TryThrowableResourceDemo2: doSomething() NullPointerException");}@Overridepublicvoidclose()throwsException{ System.out.println("TryThrowableResourceDemo2 is closed");thrownewNullPointerException("TryThrowableResourceDemo2: close() NullPointerException");}}
publicclassTryThrowableResourceDemoTest{ publicstaticvoidmain(String[]args){ try(TryThrowableResourceDemo1demo1 =newTryThrowableResourceDemo1();TryThrowableResourceDemo2demo2 =newTryThrowableResourceDemo2()){ System.out.println("do...");demo1.doSomething();demo2.doSomething();}catch(Exceptione){ System.out.println("gobal: exception");System.out.println(e.getMessage());Throwable[]suppressed =e.getSuppressed();for(inti =0;i <suppressed.length;i++){ System.out.println(suppressed[i].getMessage());}}}}
运行结果如下:
do...do something 1TryThrowableResourceDemo2 is closedTryThrowableResourceDemo1 is closedgobal: exceptionTryThrowableResourceDemo1: doSomething() NullPointerExceptionTryThrowableResourceDemo2: close() NullPointerExceptionTryThrowableResourceDemo1: close() NullPointerException
从运行结果我们可以很清晰的看到,对于try
语句块内的异常,我们可以通过e.getMessage()
获取,对于close()
方法抛出的异常,其实编译器对这部分的异常进行特殊处理,将其放入到集合数组中了,因此我们需要通过e.getSuppressed()
方法来获取。
具体反编译后的代码如下:
publicclassTryThrowableResourceDemoTest{ publicstaticvoidmain(String[]args){ try{ TryThrowableResourceDemo1demo1 =newTryThrowableResourceDemo1();Throwablevar34 =null;try{ TryThrowableResourceDemo2demo2 =newTryThrowableResourceDemo2();Throwablevar4 =null;try{ System.out.println("do...");demo1.doSomething();demo2.doSomething();}catch(Throwablevar29){ var4 =var29;throwvar29;}finally{ if(demo2 !=null){ if(var4 !=null){ try{ demo2.close();}catch(Throwablevar28){ var4.addSuppressed(var28);}}else{ demo2.close();}}}}catch(Throwablevar31){ var34 =var31;throwvar31;}finally{ if(demo1 !=null){ if(var34 !=null){ try{ demo1.close();}catch(Throwablevar27){ var34.addSuppressed(var27);}}else{ demo1.close();}}}}catch(Exceptionvar33){ System.out.println("gobal: exception");System.out.println(var33.getMessage());Throwable[]suppressed =var33.getSuppressed();for(inti =0;i <suppressed.length;++i){ System.out.println(suppressed[i].getMessage());}}}}
在实际的使用中,不管是使用try-with-resource
编程还是使用try-catch-finally
编程**,一定需要了解资源的close
方法内部的实现逻辑,否则还是可能会导致资源泄露**。
举个例子,在 Java BIO 中采用了大量的装饰器模式。当调用装饰器的 close 方法时,本质上是调用了装饰器包装的流对象的 close 方法。比如:
publicclassTryWithResource{ publicstaticvoidmain(String[]args){ try(FileInputStreamfin =newFileInputStream(newFile("input.txt"));GZIPOutputStreamout =newGZIPOutputStream(newFileOutputStream(newFile("out.txt")))){ byte[]buffer =newbyte[4096];intread;while((read =fin.read(buffer))!=-1){ out.write(buffer,0,read);}}catch(IOExceptione){ e.printStackTrace();}}}
在上述代码中,我们从FileInputStream
中读取字节,并且写入到GZIPOutputStream
中。GZIPOutputStream
实际上是FileOutputStream
的装饰器。
由于try-with-resource
的特性,实际编译之后的代码会在后面带上finally
代码块,并且在里面调用fin.close()
方法和out.close()
方法。
我们再来看GZIPOutputStream
类的close
方法。
publicvoidclose()throwsIOException{ 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
,改成如下方式:
publicclassTryWithResource{ publicstaticvoidmain(String[]args){ try(FileInputStreamfin =newFileInputStream(newFile("input.txt"));FileOutputStreamfout =newFileOutputStream(newFile("out.txt"));GZIPOutputStreamout =newGZIPOutputStream(fout)){ byte[]buffer =newbyte[4096];intread;while((read =fin.read(buffer))!=-1){ out.write(buffer,0,read);}}catch(IOExceptione){ e.printStackTrace();}}}
编译器会自动生成fout.close()
的代码,这样肯定能够保证真正的流被关闭。
在处理必须关闭的资源时,使用try-with-resources
语句替代try-catch-finally
语句,你会惊奇的发现,编写的代码更简洁,更清晰,同时也省去了手动显式释放资源的烦恼。
因此在实际编程过程中,推荐大家采用这种方式编写,同时要关注close
方法内部的实现逻辑,避免资源泄露,服务宕机!
1、知乎 - 深入理解Java try-with-resource
2、csdn - try - with - resources详解
不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!
本文已整理到技术笔记中,此外,笔记内容还涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL、微服务等技术栈。
需要的小伙伴可以点击 技术笔记 获取!