01.22 Effective-Java 使用try-with-resources

Java類庫中有許多資源需要通過close方法進行關閉。

比如 InputStream、OutputStream,數據庫連接對象 Connection,MyBatis中的 SqlSession 會話等。作為開發人員經常會忽略掉資源的關閉方法,導致內存洩漏。

引入

根據經驗,try-finally語句是確保資源會被關閉的最佳方法,就算異常或者返回也一樣。try-catch-finally 一般是這樣來用的

<code>static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
}finally {
br.close();
}
}
/<code>

這樣看起來代碼還是比較整潔,但是當我們添加第二個需要關閉的資源的時候,就像下面這樣

<code>static void copy(String src,String dst) throws Exception{
InputStream is = new FileInputStream(src);
try {

OutputStream os = new FileOutputStream(dst);
try {
byte[] buf = new byte[100];
int n;
while ((n = is.read()) >= 0){
os.write(buf,n,0);
}
}finally {
os.close();
}
}finally {
is.close();
}
}
/<code>

這樣感覺這個方法已經變得臃腫起來了。

而且這種寫法也存在諸多問題,比如:就算是 try - finally 能夠正確關閉資源,但是它不能阻止異常的拋出,因為 try 和 finally 塊中都可能有異常的發生。

比如說你正在讀取的時候硬盤損壞,這個時候你就無法讀取文件和關閉資源了,此時會拋出兩個異常。但是在這種情況下,第二個異常會抹掉第一個異常。在異常堆棧中也無法找到第一個異常的記錄,怎麼辦,難道像這樣來捕捉異常麼?

<code>static void tryThrowException(String path) throws Exception {

BufferedReader br = new BufferedReader(new FileReader(path));
try {
String s = br.readLine();
System.out.println("s = " + s);

}catch (Exception e){
e.printStackTrace();
}finally {
try {
br.close();
}catch (Exception e){
e.printStackTrace();
}finally {
br.close();
}
}
}
/<code>

這種寫法,雖然能解決異常拋出的問題,但是各種 try-cath-finally 的嵌套會讓代碼變得非常臃腫。

改變

Java7 中引入了try-with-resources 語句時,所有這些問題都能得到解決。要使用try-with-resources 語句,首先要實現 AutoCloseable 接口,此接口包含了單個返回的 close 方法。Java類庫與三方類庫中的許多類和接口,現在都實現或者擴展了 AutoCloseable 接口。如果編寫了一個類,它代表的是必須關閉的資源,那麼這個類應該實現 AutoCloseable 接口。

java引入了 try-with-resources 聲明,將 try-catch-finally 簡化為 try-catch,這其實是一種語法糖,在編譯時會進行轉化為 try-catch-finally 語句。

下面是使用 try-with-resources 的第一個範例

<code>/**
* 使用try-with-resources 改寫示例一
* @param path
* @return
* @throws IOException
*/
static String firstLineOfFileAutoClose(String path) throws IOException {

try(BufferedReader br = new BufferedReader(new FileReader(path))){
return br.readLine();
}
}
/<code>

使用 try-with-resources 改寫程序的第二個示例

<code>static void copyAutoClose(String src,String dst) throws IOException{

try(InputStream in = new FileInputStream(src);
OutputStream os = new FileOutputStream(dst)){
byte[] buf = new byte[1000];
int n;
while ((n = in.read(buf)) >= 0){
os.write(buf,0,n);
}
}
}
/<code>

使用 try-with-resources 不僅使代碼變得通俗易懂,也更容易診斷。以firstLineOfFileAutoClose方法為例,如果調用 readLine() 和 close 方法都拋出異常,後一個異常就會被禁止,以保留第一個異常。

理解

異常處理有兩種情況:

  1. try 塊沒有發生異常時,直接調用finally塊,如果 close 發生異常,直接進行處理。
  2. try 塊發生異常,catch 塊捕捉,進行第一處異常處理,然後調用 finally 塊,如果 close 發生異常,就進行第二處異常處理。

但是在 try-with-resources 結構中,異常處理也有兩種情況(注意,不論 try 中是否有異常,都會首先自動執行 close 方法,然後才判斷是否進入 catch 塊,建議閱讀後面的反編譯代碼):

  1. try 塊沒有發生異常時,自動調用 close 方法,如果發生異常,catch 塊捕捉並處理異常。
  2. try 塊發生異常,然後自動調用 close 方法,如果 close 也發生異常,catch 塊只會捕捉 try 塊拋出的異常,close 方法的異常會在catch 中被壓制,但是你可以在catch塊中,用 Throwable.getSuppressed 方法來獲取到壓制異常的數組。

下面是一個示例

<code>public class TryWithResources {

public static void testTryWithResources(){
try (MyAutoCloseA a = new MyAutoCloseA();
MyAutoCloseB b = new MyAutoCloseB()) {
a.test();
b.test();

} catch (Exception e) {
System.out.println("Main: exception");
System.out.println(e.getMessage());
Throwable[] suppressed = e.getSuppressed();
for (int i = 0; i < suppressed.length; i++)
System.out.println(suppressed[i].getMessage());
}
}


public static void main(String[] args) {
testTryWithResources();
}
}

class MyAutoCloseA implements AutoCloseable {

public void test() throws IOException {
System.out.println("MyAutoCloseA: test() ");
throw new IOException("MyAutoCloseA: test() IOException");
}

@Override
public void close() throws Exception {
System.out.println("MyAutoCloseA: on close()");
throw new ClassNotFoundException("MyAutoCloseA: close() ClassNotFoundException");
}
}

class MyAutoCloseB implements AutoCloseable {

public void test() throws IOException {
System.out.println("MyAutoCloseB: test()");
throw new IOException("MyAutoCloseB: test() IOException");
}

@Override
public void close() throws Exception {
System.out.println("MyAutoCloseB: on close()");
throw new ClassNotFoundException("MyAutoCloseB: close() ClassNotFoundException");
}
}
/<code>

輸出結果是這樣的:

MyAutoCloseA: test() MyAutoCloseB: on close() MyAutoCloseA: on close() Main: exception MyAutoCloseA: test() IOException MyAutoCloseB: close() ClassNotFoundException MyAutoCloseA: close() ClassNotFoundException

你能猜到這個輸出結果嗎?

如果有疑問的話,那麼先來看一下上面這段代碼反編譯之後的結果吧

反編譯後的執行過程

<code>public static void startTest() {
try {
MyAutoCloseA a = new MyAutoCloseA();
Throwable var33 = null;

try {
MyAutoCloseB b = new MyAutoCloseB();
Throwable var3 = null;

try { // 我們定義的 try 塊
a.test();
b.test();
} catch (Throwable var28) { // try 塊中拋出的異常
var3 = var28;
throw var28;
} finally {
if (b != null) {
// 如果 try 塊中拋出異常,就將 close 中的異常(如果有)附加為壓制異常
if (var3 != null) {
try {
b.close();
} catch (Throwable var27) {
var3.addSuppressed(var27);
}
} else { // 如果 try 塊沒有拋出異常,就直接關閉,可能會拋出關閉異常
b.close();
}
}

}
} catch (Throwable var30) {
var33 = var30;
throw var30;
} finally {
if (a != null) {

if (var33 != null) {
try {
a.close();
} catch (Throwable var26) {
var33.addSuppressed(var26);
}
} else {
a.close();
}
}
}
// 所有的異常在這裡交給 catch 塊處理
} catch (Exception var32) { // 我們定義的 catch 塊
System.out.println("Main: exception");
System.out.println(var32.getMessage());
Throwable[] suppressed = var32.getSuppressed();

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

try 塊中的關閉順序是從後向前進行關閉,也就是說,在創建完成 a 和 b 對象後,對 a 調用test() 方法,會先輸出A 的信息,然後拋出異常進行關閉,自動調用 close() 方法,執行關閉的順序是從後向前執行,所以會先關閉 b 的對象,會自動調用 b 的close 方法,然後會調用 a 的 close 方法。

因為對象 a 在執行test 方法的時候拋出了異常,所以對異常進行捕獲,輸出 Main: exception,然後獲取 A 的異常信息,因為close 方法拋出的異常在 catch 中被壓制,可以通過 Throwable.getSuppressed 進行輸出,因為 B 先調用 close() 方法出現的異常,所以先輸出了 MyAutoCloseB: close() ClassNotFoundException ,而最後輸出的是

MyAutoCloseA: close() ClassNotFoundException

總結

結論很明顯,在處理資源關閉的時候,始終要優先考慮使用 try-with-resources ,而不是 try-finally。這樣得到的代碼更加簡潔、清晰,產生的異常也更有價值。

文章參考:

《Effective Java 第三版》

https://blog.csdn.net/weixin_40255793/article/details/80812961


分享到:


相關文章: