1. 前言
在 Java(以及其他地方)處理檔案,都是在操作外部資源。當你開啟一個檔案,作業系統會為你的程式分配「檔案描述元」——一個特殊的識別符號,使你能夠讀寫檔案。這類描述元的數量有限:如果不關閉檔案,程式可能很快就會「吃光」所有可用資源,並開始拋出像 "Too many open files" 這樣令人困惑的錯誤。
此外,如果檔案未關閉,它可能會對其他程式保持鎖定。比如你以寫入模式開啟了檔案卻忘了關閉,結果不論你或其他人都無法修改或刪除它。這就像檔案系統世界中的「永無止盡的人質挾持」。
實際範例
FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... 做點事,然後忘了呼叫 fis.close()
如果不呼叫 close() 方法,檔案會一直「掛著」直到程式結束。在大型應用中,這可能導致資源洩漏,甚至讓應用程式崩潰。
2. 傳統作法: finally + close()
在 Java 7 之前,保證檔案一定會被關閉的典型寫法如下:
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
// 讀取檔案
int b = fis.read();
// ...
} catch (IOException e) {
System.out.println("讀取檔案時發生錯誤:" + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.out.println("關閉檔案時發生錯誤:" + e.getMessage());
}
}
}
此作法的缺點
- 很容易忘記加入 finally 區塊而導致資源洩漏。
- 樣板程式碼很多,尤其當有多個串流時更顯冗長。
- 關閉時若拋出例外,還得另外捕捉處理。
- 程式碼臃腫、可讀性下降。
3. 現代作法: try-with-resources
所幸,從 Java 7 起出現了優雅且自動化的語法,能解決上述問題——try-with-resources。
範例如下
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b = fis.read();
// 處理檔案
} catch (IOException e) {
System.out.println("處理檔案時發生錯誤:" + e.getMessage());
}
// 走到這裡時 fis 已自動關閉!
關鍵點:所有在 try 後括號中宣告的資源,區塊結束後都會自動關閉——即使中途拋出例外也一樣。不必寫 finally、也不需另外捕捉關閉時的錯誤——Java 會替你處理。
哪些類別可以用在 try-with-resources?
任何實作了介面 AutoCloseable(或較舊的 Closeable)的類別。這幾乎涵蓋所有標準 I/O 串流:FileInputStream、FileOutputStream、BufferedReader、BufferedWriter、Scanner、PrintWriter 等等。
4. try-with-resources 語法:細節與範例
單一資源
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("錯誤:" + e.getMessage());
}
// reader 會自動關閉!
多個資源
可以用分號一次宣告多個資源:
try (
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
} catch (IOException e) {
System.out.println("複製時發生錯誤:" + e.getMessage());
}
// 兩個串流都已關閉!
關閉順序:資源會以宣告的相反順序關閉。會先呼叫 writer.close(),再呼叫 reader.close()。若其中一個串流依賴另一個,這點很重要。
搭配自訂類別使用
如果你編寫會使用資源的自訂類別,只要實作 AutoCloseable 介面即可:
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("正在使用資源!");
}
@Override
public void close() {
System.out.println("資源已關閉!");
}
}
try (MyResource res = new MyResource()) {
res.doSomething();
}
// 離開區塊後會輸出: "資源已關閉!"
5. 運作原理:流程圖
flowchart TD
A[在 try-with-resources 中開啟資源] --> B{try 區塊中拋出例外?}
B -- 否 --> C[資源會自動關閉]
B -- 是 --> D[資源會自動關閉]
D --> E[例外會往外拋出]
C --> F[程式繼續執行]
結論:不論是否出錯,資源都一定會被關閉!
6. 範例:把程式改寫成「新寫法」
改寫前(舊風格):
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("input.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("錯誤:" + e.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
System.out.println("關閉時發生錯誤:" + e.getMessage());
}
}
}
改寫後(try-with-resources):
try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
System.out.println("錯誤:" + e.getMessage());
}
// 就這樣,不需要 finally!
7. 關閉時發生錯誤會怎樣?
有時關閉資源本身也可能拋出例外(例如檔案突然消失)。在 try-with-resources 中,這些例外不會遺失:如果在 try 區塊已拋出一個例外,而在關閉資源時又出現第二個,它會被加入為主要例外的「被抑制」(suppressed)例外。可透過 Throwable.getSuppressed() 來查看。
範例
try (MyResource res = new MyResource()) {
throw new IOException("try 區塊中的錯誤");
} catch (IOException e) {
System.out.println("主要錯誤:" + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("被抑制的例外:" + suppressed.getMessage());
}
}
8. 哪些類別支援 try-with-resources?
很簡單:任何實作了 AutoCloseable 介面的類別。以下是部分標準類別:
| 類別 | 用途 |
|---|---|
|
從檔案讀取位元組 |
|
將位元組寫入檔案 |
|
讀取/寫入文字 |
|
串流緩衝 |
|
以格式化方式寫入文字 |
|
從檔案/主控台讀取資料 |
|
序列化/反序列化 |
|
操作 ZIP 壓縮檔 |
|
網路連線 |
若你使用第三方函式庫——請查閱文件:只要有 close() 方法,類別很可能支援 try-with-resources。
9. 建議與實用細節
可在 try 之外宣告變數(自 Java 9 起):可以使用已宣告的資源,只要它們是 final 或「effectively final」:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
// ...
}
不只適用於檔案:try-with-resources 也適用於任何資源:網路連線、資料庫、任何帶有 close() 方法的物件。
不要忽略例外:即便使用 try-with-resources,也別忘了捕捉並處理例外——它不是萬靈丹,而是一種避免洩漏的便利方式。
不要在 try 內手動關閉資源:沒這個必要——交給 Java 就好!如果在區塊內手動呼叫 close(),而之後 try 區塊結束時又會再關閉一次,通常雖然安全,但可能讓人困惑。
10. 使用 try-with-resources 的常見錯誤
錯誤 1:完全沒使用 try-with-resources。如果你還在寫 finally { resource.close(); } —— 不是還停留在 2011 年,就是還沒讀過這堂課!請改用現代語法。
錯誤 2:在 try 外宣告資源,內部只是使用。這樣的程式不會自動關閉資源:
BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
// ... 使用 reader
} finally {
// 但這裡忘了關閉!
}
錯誤 3:在 try 區塊內手動呼叫 close()。這雖不致命,但多此一舉,還可能導致重複關閉。請放心交給 Java。
錯誤 4:只捕捉 Exception,忽略 I/O 特性。最好捕捉具體的例外(FileNotFoundException、IOException),才能給使用者更明確的訊息。
錯誤 5:沒有處理被抑制的例外。如果在關閉資源時發生錯誤,它可能會被「抑制」。分析錯誤時,別忘了 getSuppressed()。
GO TO FULL VERSION