HOWTO · Java
在 Java 中處理 FileNotFoundException
本教程描述並演示了 Java 中的 FileNotFoundException 以及如何處理它。
本頁內容
FileNotFoundException 是一種受檢的 java.io.IOException;當 Java 無法開啟指定路徑所命名的檔案時就會丟擲它——最常見的原因是檔案不存在、路徑指向目錄,或作業系統拒絕存取。本教學說明 FileNotFoundException 的意義,示範如何透過 try-with-resources 安全地處理它,並逐步介紹用來診斷路徑遺失與存取問題的檢查。
FileNotFoundException 在 Java 中代表什麼
java.io.FileNotFoundException 繼承自 java.io.IOException,而 java.io.IOException 又繼承自 java.lang.Exception。由於它是受檢例外,任何開啟檔案的程式碼都必須捕捉它,或以 throws 宣告它;編譯器會強制執行這項要求。
FileInputStream、FileOutputStream 和 RandomAccessFile 的建構子都宣告了它,建立在這些類別之上的 API 也一樣,例如 FileReader、FileWriter 和 new Scanner(new File(path))。當作業系統的開檔呼叫失敗時——找不到指定的檔案,或無法以要求的模式開啟——JVM 就會丟擲它。這涵蓋了幾種不同的情況,所以 FileNotFoundException 不一定代表打錯字:父目錄遺失、路徑指向目錄,或存取權限被拒,都會產生相同的型別。
Java 為什麼會丟擲 FileNotFoundException
常見的觸發原因如下:
- 程式要求的路徑上沒有該檔案,或路徑中的某個父目錄不存在。
- 路徑存在但指向目錄,而目錄無法當作一般檔案開啟。
- 檔案存在,但作業系統拒絕讓該處理程序存取它。
- 對輸出串流而言,目標無法建立,例如位於唯讀或不存在的目錄中。
像 demo.txt 這類相對路徑,是相對於處理程序的工作目錄來解析的——也就是 user.dir 系統屬性,通常是啟動 java 時所在的目錄——而不是相對於 .java 或 .class 檔案所在的目錄。程式在 IDE 中能正常運作、在終端機中卻失敗,往往只是因為執行時的工作目錄不同。
執行以下範例,可以看到一個必定失敗的開檔動作,以及聚焦的 catch;輸出會以英文顯示無法開啟的訊息與原因標籤,因為範例程式與輸出文字保持原樣:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
public class MissingFile {
public static void main(String[] args) {
String path = "missing-dir/demo.txt";
try (Scanner scanner = new Scanner(new File(path))) {
System.out.println(scanner.nextLine());
} catch (FileNotFoundException e) {
System.out.println("Cannot open " + path);
System.out.println("Cause: " + e.getClass().getName());
}
}
}
Cannot open missing-dir/demo.txt
Cause: java.io.FileNotFoundException
catch 會回報問題,而不是讓程式異常終止,因此程式能正常結束。印出 e.getMessage() 也會顯示作業系統提供的原因,但那段文字會因平台而異,所以類別名稱才是可跨平台確認的細節。
用 try-with-resources 處理 FileNotFoundException
讀取檔案最安全的方式,是在 try 區塊中開啟資源,並讓 Java 自動關閉它。這個模式稱為 try-with-resources:括號中宣告的每個變數都必須實作 AutoCloseable,Java 會以相反順序關閉所有資源——即使區塊主體提前回傳,或中途發生失敗也一樣。相較於手動 finally 清理,沒有任何參考會停留在 null,也不會有關閉呼叫被略過或遺忘。以下程式會在系統暫存目錄中寫入一個短檔案,透過 Scanner 讀回第一行,並將該行印到標準輸出。
執行此範例;輸出會以英文顯示讀取到的問候文字,因為範例程式與輸出文字保持原樣:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import java.io.File;
import java.io.FileWriter;
import java.util.Scanner;
public class ReadFirstLine {
public static void main(String[] args) throws Throwable {
File temp = File.createTempFile("delftstack", ".txt");
temp.deleteOnExit();
try (FileWriter writer = new FileWriter(temp)) {
writer.write("Hello from DelftStack!");
}
try (Scanner scanner = new Scanner(temp)) {
System.out.println(scanner.nextLine());
}
}
}
Hello from DelftStack!
任何實作 AutoCloseable 的資源——Scanner、FileReader、BufferedReader、FileInputStream 以及 NIO 通道型別——都可以放在括號中。當開啟動作本身可能失敗時,維持相同結構,並在區塊後方加上聚焦的 catch (FileNotFoundException e),如上一節所示。捕捉到 FileNotFoundException 代表開啟失敗;它不代表讀取成功,因此只有在主體確實執行後,才能回報成功。
診斷檔案遺失或存取問題
當 FileNotFoundException 出現時,先檢查路徑,再動程式碼:
- 印出解析後的位置。
new File(path).getAbsolutePath()和path.toAbsolutePath()會顯示程式實際尋找的位置,而System.getProperty("user.dir")會顯示相對路徑所依據的工作目錄。 - 檢查每個路徑區段的拼字與大小寫。在 Linux 上,檔名區分大小寫,因此
Demo.txt和demo.txt是不同的檔案;macOS 和 Windows 的預設檔案系統不區分大小寫,但名稱仍必須與儲存時完全一致。當真實副檔名被隱藏時,編輯器也可能把檔案存成demo.txt.txt。 - 檢查路徑是否指向目錄。對目錄進行讀取或寫入的開啟動作會以同樣方式失敗。
- 檢查存取權限。
Files.isReadable和Files.isWritable會回報該處理程序可執行的動作;在 Windows 上,被其他程式鎖定的檔案也可能無法開啟。 - 對輸出檔案,確認父目錄存在且可寫入。
下面的 java.nio.file.Files 探測會在不開啟檔案的情況下,明確執行前述檢查;輸出會以英文顯示各探測名稱與結果值,因為範例程式與輸出文字保持原樣:
本節中的程式碼以及顯示的標籤或輸出仍為英文。
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CheckPath {
public static void main(String[] args) {
Path path = Paths.get("missing-dir", "demo.txt");
System.out.println("exists=" + Files.exists(path));
System.out.println("isDirectory=" + Files.isDirectory(path));
System.out.println("isRegularFile=" + Files.isRegularFile(path));
System.out.println("isReadable=" + Files.isReadable(path));
}
}
exists=false
isDirectory=false
isRegularFile=false
isReadable=false
當每個探測都回傳 false 時,表示該路徑所指的目標對此處理程序完全不可見——這是拼字、工作目錄或檔案遺失的問題,而不是程式碼錯誤。
選擇正確的例外處理方式
當檔案遺失有專屬的補救方式時,明確捕捉 FileNotFoundException:退回預設設定、建立該檔案,或提示使用者提供另一個路徑。當開啟失敗與讀寫失敗都採用相同的處理方式時,捕捉較廣的 IOException——例如整個操作共用同一個記錄後中止的流程。如果兩個處理器都寫,FileNotFoundException 的 catch 必須放在前面,因為它是子類別。
當決定權屬於呼叫者時,就宣告 throws——只負責開啟呼叫者指定之物的函式庫,應該讓失敗向上傳遞。無論選擇哪種方式,都不要吞掉例外,也不要把捕捉到的失敗當作成功:記錄它、回傳清楚的結果,或在檔案遺失代表程式或部署錯誤時,以 IllegalStateException 這類非受檢型別重新丟擲。