為什么推薦大家優(yōu)先使用 try-with-resources 而非 try-finally
一、背景介紹
try-with-resources是 JDK 7 中引入的一個(gè)新的異常處理機(jī)制,它能讓開(kāi)發(fā)人員不用顯式的釋放try-catch語(yǔ)句塊中使用的資源。
比如,我們以文件資源拷貝為示例,大家所熟悉的try-catch-finally寫(xiě)法如下:
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 {
//關(guān)閉文件流
if (bin != null) {
try {
bin.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (bout != null) {
try {
bout.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
我們現(xiàn)在將其改成使用try-with-resources編程方式,你會(huì)驚奇的發(fā)現(xiàn)只需要簡(jiǎn)單的幾行代碼就可以搞定,不用顯式關(guān)閉資源,方式如下:
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 之前,在處理必須關(guān)閉的資源時(shí),開(kāi)發(fā)人員必須要牢記在try-catch語(yǔ)句中使用finally執(zhí)行關(guān)閉資源的方法,否則隨著程序不斷運(yùn)行,資源泄露將會(huì)累計(jì)成重大的生產(chǎn)事故,如果你的程序中同時(shí)打開(kāi)了多個(gè)資源,你會(huì)驚奇的發(fā),關(guān)閉資源的代碼竟然比業(yè)務(wù)代碼還要多,使得代碼更加難以清晰的閱讀和管理。
因此在這樣的背景下,try-with-resources由此誕生,它的設(shè)計(jì)初衷就是旨在減輕開(kāi)發(fā)人員釋放try塊中使用的資源負(fù)擔(dān)。
習(xí)慣了try-catch-finally寫(xiě)法的同學(xué),可能會(huì)發(fā)出疑問(wèn),是不是所有涉及到資源的操作都可以用try-with-resources編程?使用這種編程方式有沒(méi)有坑?如果有坑,使用的時(shí)候哪些地方應(yīng)該需要注意呢?....
好吧,廢話也不多說(shuō)了,今天我們就一起來(lái)看看try-with-resources編程原理。
二、實(shí)踐解說(shuō)
try-with-resources語(yǔ)句能確保每個(gè)資源在語(yǔ)句結(jié)束時(shí)被關(guān)閉,但是有一個(gè)前提條件,那就是這個(gè)資源必須實(shí)現(xiàn)了java.lang.AutoCloseable接口,才可以被執(zhí)行關(guān)閉。
try-with-resources編程模式中,無(wú)需開(kāi)發(fā)人員顯式關(guān)閉資源的前提是,這個(gè)資源必須實(shí)現(xiàn)java.lang.AutoCloseable接口,并且重寫(xiě)close方法,否則無(wú)法在try-with-resources中進(jìn)行聲明變量。
下面我們可以關(guān)閉單個(gè)資源為例,代碼如下:
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();
}
}
}
運(yùn)行結(jié)果如下:
do something
resource is closed
可以很清晰的看到,close方法被調(diào)用了!
下面我們?cè)俅蜷_(kāi)反編譯后的TryResourceTest.class文件代碼,你會(huì)驚奇發(fā)現(xiàn),編譯器自動(dòng)給代碼加上了finally方法,并且會(huì)調(diào)用close方法,將資源關(guān)閉!
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();
}
}
}
也就是說(shuō),使用try-with-resources編程,其實(shí)是編譯器顯式的給代碼了添加finally方法,省去開(kāi)發(fā)人員手動(dòng)關(guān)閉資源的操作!
三、資源關(guān)閉順序
上面我們只介紹了關(guān)閉單個(gè)資源的場(chǎng)景,假如有多個(gè)資源時(shí),try-with-resources是如何關(guān)閉的呢?
下面還是舉例看結(jié)果。
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();
}
}
}
運(yùn)行結(jié)果如下:
do...
do something 1
do something 2
resource 2 is closed
resource 1 is closed
從結(jié)果上可以看出,try語(yǔ)句中越是最后使用的資源,越是最早被關(guān)閉。
關(guān)于這一點(diǎn),大家可以從反編譯的代碼中找到原理!
四、異常處理機(jī)制
正常的情況下,try語(yǔ)句結(jié)束時(shí)會(huì)關(guān)閉相關(guān)的資源,假如語(yǔ)句內(nèi)部執(zhí)行時(shí)發(fā)生異常,同時(shí)我們又顯式的調(diào)用了finally方法,執(zhí)行的順序又是怎樣的呢?
下面繼續(xù)舉例看結(jié)果。
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();
}
}
}
}
運(yùn)行結(jié)果如下:
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)
可以很清晰的看到,可以得出如下結(jié)論:
- 1.只要實(shí)現(xiàn)了AutoCloseable接口的類(lèi),并且在try里聲明了對(duì)象變量,在try結(jié)束后,不管是否發(fā)生異常,close方法都會(huì)被調(diào)用
- 2.其次,在try里越晚聲明的對(duì)象,會(huì)越早被close掉
- 3.try結(jié)束后自動(dòng)調(diào)用的close方法,這個(gè)動(dòng)作會(huì)早于finally里調(diào)用的方法
五、壓制異常處理
大部分情況,我們通常不會(huì)擔(dān)心資源的close會(huì)發(fā)生異常,現(xiàn)在假設(shè)如果try里聲明的資源對(duì)象,當(dāng)執(zhí)行close方法拋異常時(shí),他們的執(zhí)行順序又是怎樣的呢?我們又如何獲取這種異常呢?
還是眼見(jiàn)為實(shí),下面以舉例看結(jié)果。
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());
}
}
}
}
運(yùn)行結(jié)果如下:
do...
do something 1
TryThrowableResourceDemo2 is closed
TryThrowableResourceDemo1 is closed
gobal: exception
TryThrowableResourceDemo1: doSomething() NullPointerException
TryThrowableResourceDemo2: close() NullPointerException
TryThrowableResourceDemo1: close() NullPointerException
從運(yùn)行結(jié)果我們可以很清晰的看到,對(duì)于try語(yǔ)句塊內(nèi)的異常,我們可以通過(guò)e.getMessage()獲取,對(duì)于close()方法拋出的異常,其實(shí)編譯器對(duì)這部分的異常進(jìn)行特殊處理,將其放入到集合數(shù)組中了,因此我們需要通過(guò)e.getSuppressed()方法來(lái)獲取。
具體反編譯后的代碼如下:
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());
}
}
}
}
六、關(guān)閉資源的坑
在實(shí)際的使用中,不管是使用try-with-resource編程還是使用try-catch-finally編程,一定需要了解資源的close方法內(nèi)部的實(shí)現(xiàn)邏輯,否則還是可能會(huì)導(dǎo)致資源泄露。
舉個(gè)例子,在 Java BIO 中采用了大量的裝飾器模式。當(dāng)調(diào)用裝飾器的 close 方法時(shí),本質(zhì)上是調(diào)用了裝飾器包裝的流對(duì)象的 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中讀取字節(jié),并且寫(xiě)入到GZIPOutputStream中。GZIPOutputStream實(shí)際上是FileOutputStream的裝飾器。
由于try-with-resource的特性,實(shí)際編譯之后的代碼會(huì)在后面帶上finally代碼塊,并且在里面調(diào)用fin.close()方法和out.close()方法。
我們?cè)賮?lái)看GZIPOutputStream類(lèi)的close方法。
public void close() throws IOException {
if (!closed) {
finish();
if (usesDefaultDeflater)
def.end();
out.close();
closed = true;
}
}
在調(diào)用out變量的close方法之前,GZIPOutputStream還做了finish操作,該操作還會(huì)繼續(xù)往FileOutputStream中寫(xiě)壓縮信息,此時(shí)如果出現(xiàn)異常,則out.close()方法會(huì)被略過(guò),而out變量實(shí)際上代表的是被裝飾的FileOutputStream類(lèi),這個(gè)才是最底層的資源關(guān)閉方法。
正確的做法應(yīng)該是在try-with-resource中單獨(dú)聲明最底層的資源,保證對(duì)應(yīng)的close方法一定能夠被調(diào)用。在剛才的例子中,我們需要單獨(dú)聲明每個(gè)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();
}
}
}
編譯器會(huì)自動(dòng)生成fout.close()的代碼,這樣肯定能夠保證真正的流被關(guān)閉。
七、小結(jié)
在處理必須關(guān)閉的資源時(shí),使用try-with-resources語(yǔ)句替代try-catch-finally語(yǔ)句,你會(huì)驚奇的發(fā)現(xiàn),編寫(xiě)的代碼更簡(jiǎn)潔,更清晰,同時(shí)也省去了手動(dòng)顯式釋放資源的煩惱。
因此在實(shí)際編程過(guò)程中,推薦大家采用這種方式編寫(xiě),同時(shí)要關(guān)注close方法內(nèi)部的實(shí)現(xiàn)邏輯,避免資源泄露,服務(wù)宕機(jī)!