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 就不會被關閉。
你想要的是無論如何,最後一定要執行關閉資源的動作,try、catch 語法還可以搭配 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;
}
}
自動嘗試關閉資源
經常地,在使用 try、finally 嘗試關閉資源時,會發現程式撰寫的流程是類似的,就如 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被關閉


