與面試官聊try-catch-finally關(guān)閉資源,你的答案還是10年前的?
前言
有編程經(jīng)驗(yàn)的朋友都知道,在程序運(yùn)行中如果打開了一些資源,那么當(dāng)發(fā)生異常或程序結(jié)束時(shí)都需要進(jìn)行資源的關(guān)閉,不然會(huì)造成內(nèi)存溢出的問題。
曾經(jīng),關(guān)于try-catch-finally的使用也是面試題中的一個(gè)熱點(diǎn)問題。隨著JDK7的發(fā)布,情況好像有些變化了,處理資源關(guān)閉的方式更加方便了。但如果你的使用方式依舊停留在十年前,那這篇文章中講到的知識點(diǎn)值得你一讀。最重要的是底層原理分析部分。
try-catch-finally傳統(tǒng)處理模式
在JDK7之前,我們對異常和資源關(guān)閉的處理,通常是通過下面的形式來實(shí)現(xiàn)的:
@Test
public void testOldProcess() {
Scanner scanner = null;
try {
scanner = new Scanner(new File("test.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
}
首先,通過try-catch來捕獲異常,并在catch代碼塊中對異常進(jìn)行處理(比如打印日志等);
其次,在finally代碼塊中對打開的資源進(jìn)行關(guān)閉。因?yàn)闊o論程序是否發(fā)生異常,finally代碼塊是必然會(huì)被執(zhí)行的,這也就保證了資源的關(guān)閉。
當(dāng)你寫了多年的代碼,上面的寫法也已經(jīng)牢記于心,但如果用JDK7及以上版本,且IDE中安裝了一些代碼規(guī)范的插件,在try上面會(huì)有如下提示:
'try' can use automatic resource management
提示告訴你,try中的代碼可以使用自動(dòng)資源管理了。那我們就來看看它是如何實(shí)現(xiàn)自動(dòng)管理的呢。
JDK7的資源關(guān)閉方式
JDK7中引入了一個(gè)新特性:“try-with-resource”。先將上面的代碼改造成新的實(shí)現(xiàn)方式:
@Test
public void testNewProcess() {
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
在try后面添加一個(gè)小括號,在小括號內(nèi)聲明初始化操作的資源。此時(shí),我們再也不用寫finally代碼塊進(jìn)行資源的關(guān)閉了,JVM會(huì)替我們進(jìn)行資源管理,自動(dòng)關(guān)閉資源。
如果需要聲明多個(gè)資源,則可以通過分號進(jìn)行分割:
@Test
public void testNewProcess1() {
try (
Scanner scanner = new Scanner(new File("test.txt"));
Scanner scanner1 = new Scanner(new File("test1.txt"));) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
while (scanner1.hasNext()) {
System.out.println(scanner1.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
那么是不是,所有的資源都可以被JVM自動(dòng)關(guān)閉呢?還真不是的,對應(yīng)的資源類要實(shí)現(xiàn)java.io.Closeable接口才行。比如上面的Scanner便是實(shí)現(xiàn)了此接口:
public final class Scanner implements Iterator<String>, Closeable {//...}
自定義關(guān)閉實(shí)現(xiàn)
既然實(shí)現(xiàn)java.io.Closeable接口的類可以享受自動(dòng)關(guān)閉資源的好處,那我們自定義類是否同樣享受這個(gè)福利呢?
先定義一個(gè)MyResource類,實(shí)現(xiàn)java.io.Closeable接口:
public class MyResource implements Closeable {
public void hello(){
System.out.println("Hello try-catch-resource");
}
@Override
public void close() throws IOException {
System.out.println("自定義的close方法被自動(dòng)調(diào)用了...");
}
}
在自定義類中要實(shí)現(xiàn)close()方法。然后看一下使用時(shí)是否會(huì)被自動(dòng)關(guān)閉:
@Test
public void testMyResource() {
try (MyResource resource = new MyResource();) {
resource.hello();
} catch (IOException exception) {
exception.printStackTrace();
}
}
執(zhí)行單元測試,輸入結(jié)果:
Hello try-catch-resource
自定義的close方法被自動(dòng)調(diào)用了...
可以看到在調(diào)用hello方法之后,JVM自動(dòng)調(diào)用了close方法,完美的關(guān)閉了資源。
底層實(shí)現(xiàn)
了解我寫文章風(fēng)格的讀者都會(huì)知道,在寫一個(gè)知識點(diǎn)時(shí)我們不只會(huì)停留在表面,還要看一下它的底層實(shí)現(xiàn)。這里我們先將測試代碼簡化:
public void testMyResource() {
try (MyResource resource = new MyResource()) {
resource.hello();
} catch (IOException e) {
e.printStackTrace();
}
}
然后對其class文件進(jìn)行反編譯,可以看到Java編譯器對這一些寫法的真正實(shí)現(xiàn):
public void testMyResource() {
try {
MyResource resource = new MyResource();
Throwable var2 = null;
try {
resource.hello();
} catch (Throwable var12) {
var2 = var12;
throw var12;
} finally {
if (resource != null) {
if (var2 != null) {
try {
resource.close();
} catch (Throwable var11) {
var2.addSuppressed(var11);
}
} else {
resource.close();
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
會(huì)發(fā)現(xiàn)雖然我們沒寫finally代碼塊進(jìn)行資源的關(guān)閉,但Java編譯器已經(jīng)幫我們做了處理??吹竭@里,你可能已經(jīng)意識到了,try-catch-resource這種寫法只是一個(gè)語法糖。
但好像不僅僅如此,finally代碼中還包含了一個(gè)addSuppressed方法的調(diào)用,這又是怎么回事呢?下面來分析一下。
避免異常覆蓋
在上面的示例中,我們將MyResource的兩個(gè)方法進(jìn)行改造:
public class MyResource implements Closeable {
public void hello(){
throw new RuntimeException("Resource throw Exception...");
}
@Override
public void close() {
throw new RuntimeException("Close method throw Exception...");
}
}
在兩個(gè)方法中都拋出異常,此時(shí),我們再來執(zhí)行一下傳統(tǒng)寫法的單元測試代碼:
@Test
public void testOldMyResource() {
MyResource resource = null;
try {
resource = new MyResource();
resource.hello();
} finally {
if (resource != null) {
resource.close();
}
}
}
打印結(jié)果如下:
java.lang.RuntimeException: Close method throw Exception...
at com.secbro2.resource.MyResource.close(MyResource.java:19)
at com.secbro2.resource.CloseMyResourcesTest.testOldMyResource(CloseMyResourcesTest.java:22)
//...
你發(fā)現(xiàn)什么了?本來是hello方法先拋出了異常,然后執(zhí)行close方法又拋出了異常,但后面的異常信息將前面真正的異常信息給“隱藏”了。此時(shí)你去排查bug,是不是很困惑?最關(guān)鍵的異常信息被覆蓋了。
那么,我們再來執(zhí)行一下try-catch-resource寫法的代碼:
@Test
public void testMyResource() {
try (MyResource resource = new MyResource()) {
resource.hello();
}
}
執(zhí)行結(jié)果如下:
java.lang.RuntimeException: Resource throw Exception...
at com.secbro2.resource.MyResource.hello(MyResource.java:14)
at com.secbro2.resource.CloseMyResourcesTest.testMyResource(CloseMyResourcesTest.java:30)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Suppressed: java.lang.RuntimeException: Close method throw Exception...
at com.secbro2.resource.MyResource.close(MyResource.java:19)
at com.secbro2.resource.CloseMyResourcesTest.testMyResource(CloseMyResourcesTest.java:31)
... 22 more
此時(shí)hello方法中的異常信息和close方法中的異常信息全被打印出來了。而異常信息中多出的Suppressed提示便是通過Java編譯器自動(dòng)添加的addSuppressed方法的調(diào)用來實(shí)現(xiàn)的。此時(shí),再通過異常日志排查bug是不是簡單多了,編譯器是真為程序員著想啊。
小結(jié)
本文通過對try-catch-finally和try-with-resource兩種寫法的對比,得知try-with-resource是JDK7為我們提供的一個(gè)語法糖,可以讓我們的代碼更加簡潔,本質(zhì)上與try-catch-finally的效果一樣。同時(shí),try-with-resource寫法通過addSuppressed方法對異常覆蓋問題進(jìn)行了處理,更便于程序員排查bug。
往期推薦
如果你覺得這篇文章不錯(cuò),那么,下篇通常會(huì)更好。添加微信好友,可備注“加群”(微信號:zhuan2quan)。
和花一輩子都看不清的人,
注定是截然不同的搬磚生涯。



