finally 資源關閉

June 19, 2022

要抓還是要拋?〉撰寫的 FileUtil 範例並不是很正確,如果建構 FileInputStream 實例就會開啟檔案,不使用時,應該呼叫 close 關閉檔案。FileUtil 是透過 Scanner 搭配 FileInputStream 來讀取檔案,實際上 Scanner 物件有個 close 方法,可以關閉 Scanner 相關資源與搭配的 FileInputStream`。

使用 finally

那麼要何時關閉資源呢?如下撰寫並不是很正確:

...
public static String readFile(String name) throws FileNotFoundException {
    var builder = new StringBuilder();
    var scanner = new Scanner(new FileInputStream(name));
    while (scanner.hasNext()) {
        builder.append(scanner.nextLine());
        builder.append('\n');
    }
    scanner.close();
    return builder.toString();
}
...

如果 scanner.close 前發生了任何例外,執行流程就會中斷,因此 scanner.close 就可能不會執行,因此 Scanner 及搭配的 FileInputStream 就不會被關閉。

你想要的是無論如何,最後一定要執行關閉資源的動作,trycatch 語法還可以搭配 finally,無論 try 區塊有無發生例外,若撰寫有 finally 區塊,finally 區塊一定會被執行。例如:

package cc.openhome;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileUtil {
    public static String readFile(String name) throws FileNotFoundException {
        var builder = new StringBuilder();
        Scanner scanner = null;
        try {
            scanner = new Scanner(new FileInputStream(name));
            while (scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
        } finally {
            if(scanner != null) {
                scanner.close();
            }
        }
        return builder.toString();
    }
}

由於 finally 區塊一定會被執行,這個範例中 scanner 原先是 null,若 FileInputStream 建構失敗,scanner 就有可能還是 null,因此在 finally 區塊必須先檢查 scanner 是否有參考物件,有的話才進一步呼叫 close 方法,否則 scanner 參考至 null 又打算呼叫 close 方法,會拋出 NullPointerException

如果程式撰寫的流程中先 return 了,而且也有寫 finally 區塊,那 finally 區塊會先執行完後,再將值傳回。例如,下面這個範例會先顯示「finally…」再顯示「1」:

package cc.openhome;

public class Main {    
    public static void main(String[] args) {
        System.out.println(test(true));
    }

    static int test(boolean flag) {
        try {
            if(flag) {
                return 1;
            }
        } finally {
            System.out.println("finally...");
        }
        return 0;
    }
}

自動嘗試關閉資源

經常地,在使用 tryfinally 嘗試關閉資源時,會發現程式撰寫的流程是類似的,就如 FileUtil 示範的,你會先檢查 scanner 是否為 null,再呼叫 close 方法關閉 Scanner

如果你是這類流程,可以使用嘗試關閉資源(try-with-resources)語法,直接來看如何使用:

package cc.openhome;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.Scanner;

public class FileUtil2 {
    public static String readFile(String name) throws FileNotFoundException {
        var builder = new StringBuilder();
        try(var scanner = new Scanner(new FileInputStream(name))) {
            while (scanner.hasNext()) {
                builder.append(scanner.nextLine());
                builder.append('\n');
            }
        } 
        return builder.toString();
    }
}

正如程式示範的,想要嘗試自動關閉資源的物件,是撰寫在 try 之後的括號中,如果無需 catch 處理任何例外,可以不用撰寫,也不用撰寫 finally 自行嘗試關閉資源。

使用自動嘗試關閉資源語法時,也可以搭配 catch。例如也許你想在發生 ``FileNotFoundException` 時顯示堆疊追蹤訊息:

...
public static String readFile(String name) throws FileNotFoundException {
    var builder = new StringBuilder();
    try(var scanner = new Scanner(new FileInputStream(name))) {
        while (scanner.hasNext()) {
            builder.append(scanner.nextLine());
            builder.append('\n');
        }
    } catch(FileNotFoundException ex) {
        ex.printStackTrace();
        throw ex;
    }
    return builder.toString();
}
...

使用自動嘗試關閉資源語法時,並不影響你對特定例外的處理,實際上,自動嘗試關閉資源語法也僅協助你關閉資源,而非用於處理例外。

AutoClosable

嘗試關閉資源語法可套用的物件,必須實作 java.lang.AutoCloseable 介面,以下是個簡單示範:

package cc.openhome;

public class Main {    
    public static void main(String[] args) {
        try(var res = new Resource()) {
            res.doSome();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

class Resource implements AutoCloseable {
    void doSome() {
        System.out.println("做一些事");
    }
    @Override
    public void close() throws Exception {
        System.out.println("資源被關閉");
    }
}

執行結果如下:

做一些事
資源被關閉

嘗試關閉資源語法也可以同時關閉兩個以上的物件資源,只要中間以分號區隔。來看看以下的範例,哪個物件資源會先被關閉呢?

package cc.openhome;

public class Main2 {    
    public static void main(String[] args) {
        try(ResourceSome some = new ResourceSome();
             ResourceOther other = new ResourceOther()) {
            some.doSome();
            other.doOther();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

class ResourceSome implements AutoCloseable {
    void doSome() {
        System.out.println("做一些事");
    }
    @Override
    public void close() throws Exception {
        System.out.println("資源Some被關閉");
    }
}

class ResourceOther implements AutoCloseable {
    void doOther() {
        System.out.println("做其它事");
    }
    @Override
    public void close() throws Exception {
        System.out.println("資源Other被關閉");
    }
}

try 的括號中,越後面撰寫的物件資源會越早被關閉。執行結果如下,ResourceOther 實例會先被關閉,然後再關閉 ResourceSome 實例:

做一些事
做其它事
資源Other被關閉
資源Some被關閉

分享到 LinkedIn 分享到 Facebook 分享到 Twitter