---
# System prepended metadata

title: Java try-with-resource很方便，但是真的能夠相信它嗎? 以java.sql中的資料庫物件們進行探討

---

# Java try-with-resource很方便，但是真的能夠相信它嗎? 以java.sql中的資料庫物件們進行探討

## 前言
程式裡面經常需要處理資源，所以會借助一些能夠處理資源的類別，來協助我們操控資源。

馬上先來看看以下的例子:
```java
import java.io.ByteArrayInputStream;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class TryWithResouceNote {

	public static void main(String[] args) {
		byte[] data = initByteData();
		String contentInFile = readByteArray(data);
		System.out.print(contentInFile);
	}
	
	
	public static String readByteArray(byte[] data) {
		ByteArrayInputStream bf = new ByteArrayInputStream(data);
		int c;
		StringBuilder sb = new StringBuilder();
		
		while ((c = bf.read()) != -1) {
			sb.append((char) c);
		}
		
		return sb.toString();
	}
	
	public static byte[] initByteData() {
		StringBuilder sb = new StringBuilder();
		sb.append("Hi, my name is Kevin Chen,\n");
		sb.append("this article will discuss the problem of resource usage,\n");
		
		Date today = Calendar.getInstance().getTime();
		SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd");
		sb.append("Today is: ").append(formater.format(today));
		return sb.toString().getBytes();
	}
	
}
```

在程式裡面，讀取資料的程式被抽成一個函數```readByteArray(byte[])```，它專門負責將byte[]轉換為String，並且裡面使用了BufferedInputStream讀取byte[]，但是在函數結束之後，因為BufferedInputStream沒有被關閉，所以byte[]這個資源也沒有辦法被釋放，這樣可能就會產生資源洩漏的問題。同樣的狀況可能也會發生在讀取**檔案**的程式，只要沒有對物件做關閉，就會導致資源無法順利地被釋放。

## 記得.close()
在JAVA裡面，能夠操控資源的物件，常見的有:
Reader, Writer, Inputstream, Outputstream
* 這些物件可以用來讀取檔案、系統屬性，或是像上面的程式範例讀取byte[]
* 列舉出來的都只是Interface，每個Interface下面會有各自的實作類別，例如Reader下面的實作類別就有BufferedReader、InputStreamReader、FileReader...(類別繁多不一一列舉)

DataSource, Connection, Statement, Resultset
* 這些物件可以讓JAVA與資料庫進行互動，例如在應用程式啟動時可以建立連線池、利用連線進行SQL查詢、處理SQL查詢的結果等等

當程式有使用到以上的物件，不再需要使用的時候，記得把物件進行關閉，只需要調用物件的```.close()```方法:
```java
public static String readByteArray(byte[] data) {
    ByteArrayInputStream bf = null;
    try {
        bf = new ByteArrayInputStream(data);

        int c;
        StringBuilder sb = new StringBuilder();

        while ((c = bf.read()) != -1) {
            sb.append((char) c);
        }

        return sb.toString();
    } catch (Exception e) {
        throw new RuntimeException("read Byte Array Failed", e);
    } finally {
        if (bf != null) {
            try {
                bf.close();
            } catch (IOException e) {
                System.out.println("close byte array input stream failed");
            }
        }
    }
}
```

JAVA 7裡面引入的語法糖```Try-with-resource```，讓我們不必明確的在程式裡面撰寫```.close()```方法，因為語法糖會幫助我們，自動將```try()```裡面所宣告的的物件進行關閉。

所以，我們可以將程式進行簡化:
```java
public static String readByteArray(byte[] data) {
    try (ByteArrayInputStream bf = new ByteArrayInputStream(data)) {
        int c;
        StringBuilder sb = new StringBuilder();
			
        while ((c = bf.read()) != -1) {
            sb.append((char) c);
        }
			
        return sb.toString();
    } catch (Exception e) {
        throw new RuntimeException("read Byte Array Failed", e);
    }
}
```

## 保持質疑
有了語法糖的幫助，確實是能夠讓程式變得比較簡化一點，但是在程式離開try區塊之後, 那些在```try()```宣告的物件，真的會如預期地自動關閉嗎?

近期在工作的時候，處理到了下SQL到DB進行資料查詢的程式，原先是直接使用try-with-resource的語法進行撰寫，所以非常直覺的將Connection、Statement這些物件全部都包到```try()```裡面進行宣告，所以有了以下的程式:
```java
public static List<Map<String, Object>> selectBySQL(String sql) {
    try(Connection connection = dataSource.getConnection();
        PreparedStatement ps = connection.prepareStatement(sql)) {

        try (ResultSet rs = ps.executeQuery()) {
            //process data in ResultSet...
        }

    } catch (SQLException e) {
        throw new RuntimeException("failed to query data by SQL", e);
    }
}
```

在上面的程式裡面，可以發現其實各個物件是層層相依的:
* Connection依賴DataSource
* PreparedStatement依賴Connection
* ResultSet依賴PreparedStatement

但是後來在網上搜尋發現這篇討論串，理解到這似乎不是一個很好的寫法:
[Must JDBC Resultsets and Statements be closed separately although the Connection is closed afterwards?](https://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection)

即使有語法糖，也無法保證所有相關物件都會被自動關閉。
有人就遇到了問題
1. 使用某廠家的Driver實作，即使將Connection物件關閉，其他相依的物件:ResultSet、PreparedStatement**卻沒有如預期地關閉**，導致資源洩漏的問題產生。
    (ResultSet物件可以想像成是一個資料的結果集，它**會佔據JVM的記憶體空間**，如果這個結果集越大、資料量越多，它所佔據的記憶體也會越多。)
2. 從依賴關係可以知道ResultSet會去依賴Statement，Statement會去依賴Connection，所以理當來說關閉Connection物件後，由它所創造出來的Statement與ResultSet物件應該也會一起被關閉，到[Connection javadoc](https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#close--)查看，調用```.close()```會將Connection物件相關的資源進行釋放。
    > Releases this Connection object's database and JDBC resources immediately instead of waiting for them to be automatically released.

    不過還是有人遇到了問題，即使它關閉了Connection，Statement與ResultSet卻沒有被關閉，至DB查看，ResultSet所關聯的Cursor物件仍然保持開啟，最終導致應用掛掉。
    所以它改寫程式，一併將Statement進行關閉，結果問題就沒有再發生了，原因竟然是因為沒有關閉Statement而導致Cursor持續開啟。

相關的討論文不計其數，就此篇討論文來看，有正反兩派的說法:
有些人覺得Connection關閉了其他物件都會被關閉，所以使用語法糖的寫法就夠了；另一派則覺得不要過度依賴語法糖自動關閉的機制，因為有時候無法預期JAVA何時會將物件關閉，也無法知道物件何時會被垃圾回收。

以開發者的角度來看，如果確定了物件之後不會再被程式使用到，進行**顯式關閉**(調用```物件.close()```, explicitly close)也是個可嘗試的選項，這會讓JVM更快的將物件進行回收，並且也能夠將記憶體重新被分配的時間提前。

語法糖其實也沒有說不好，畢竟它能夠簡化許多行程式的撰寫，減少許多```.close()```的程式，在大部分的使用案例中，它還是能夠如期運作，它使用了**隱式關閉**(implicitly close)，就是說Java會幫你把物件進行關閉，**不過你不會知道物件何時被關閉**。

> 從這個例子也學習到，即使廠家們都是依照一個規範好的介面去實作自家的功能，但可能不是每家都有好好地遵循規則進行程式開發，文章中留言區提到的例子，筆者並未特別去研究是否真有此事，但有這樣的例子出現，還是警惕了身為開發者的我，要記得多去理解一下自己程式的寫法，會不會造成什麼不可預測的影響。

然後又在討論串裡面發現這個十幾年前的文章，講述了最安全的寫法:
 [How to Close JDBC Resources Properly – Every Time](https://shinesolutions.com/2007/08/04/how-to-close-jdbc-resources-properly-every-time/)

摘要重點:
* 將物件進行分層宣告
* 物件初始化的時候，不要給它null作為初始值，應該要立即指派一個真實的物件給它
* 一個物件宣告完成後，下面接著一個try區塊，並且在finally區塊手動關閉這個物件

其餘細節文章裡面講得非常詳細，這裡就來應用在前面的範例程式上，改寫看看:
```java
public static List<Map<String, Object>> selectBySQL(String sql) throws SQLException {
    Connection connection = dataSource.getConnection();
    try {
        PreparedStatement ps = connection.prepareStatement(sql);
        try {
            ResultSet rs = ps.executeQuery();
            try {
                //process data in ResultSet...
            } finally {
                rs.close();
            }

        } finally {
            ps.close();
        }

    } catch (SQLException e) {
        throw new RuntimeException("failed to query data by SQL", e);
    } finally {
        connection.close();
    }
}
```

## 總結
- 不要太相信自動關閉物件的這個機制，不要過度依賴語法糖。
- 物件宣告分層的概念，可以運用到所有需要調用```.close()```的情境，或是某些物件會有```.destroy()```、```.dispose()```等等。
- 關閉物件的寫法很多種，使用哪種看個人偏好，最主要的是養成習慣，隨手關閉沒有用到的物件。
- 對於不會再被使用的物件進行顯式關閉，也是個可嘗試的選項，畢竟有時候```.close()```的實作內容是會按照廠家的實作而變化的。