CodeGym /課程 /JAVA 25 SELF /使用 try-with-resources:自動關閉資源

使用 try-with-resources:自動關閉資源

JAVA 25 SELF
等級 36 , 課堂 4
開放

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 串流:FileInputStreamFileOutputStreamBufferedReaderBufferedWriterScannerPrintWriter 等等。

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 介面的類別。以下是部分標準類別:

類別 用途
FileInputStream
從檔案讀取位元組
FileOutputStream
將位元組寫入檔案
FileReader/FileWriter
讀取/寫入文字
BufferedReader/Writer
串流緩衝
PrintWriter
以格式化方式寫入文字
Scanner
從檔案/主控台讀取資料
ObjectInputStream/Output
序列化/反序列化
ZipInputStream/Output
操作 ZIP 壓縮檔
Socket, ServerSocket
網路連線

若你使用第三方函式庫——請查閱文件:只要有 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 特性。最好捕捉具體的例外(FileNotFoundExceptionIOException),才能給使用者更明確的訊息。

錯誤 5:沒有處理被抑制的例外。如果在關閉資源時發生錯誤,它可能會被「抑制」。分析錯誤時,別忘了 getSuppressed()

1
問卷/小測驗
讀取與寫入檔案,等級 36,課堂 4
未開放
讀取與寫入檔案
讀取與寫入檔案
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION