1. 輸入/輸出例外的階層
在 Java 中,處理檔案與其他外部資源時,幾乎一定會遇到所謂的輸入/輸出例外。當讀寫資料出現問題時,會拋出(throw)對應的物件。例如,找不到檔案、沒有權限,或磁碟突然「累了」。
本講的主角
Java 對這類例外有完整的繼承階層。以下是幾個主要類別:
- IOException — 所有 I/O 錯誤的基底類別。只要檔案、串流或網路出了問題,幾乎都與它(或其眾多子類)有關。
- FileNotFoundException — IOException 的「兒子」,當你嘗試開啟不存在的檔案,或路徑有誤時就會出現。
其他衍生類別:
- EOFException — 也是 IOException 的直接「兒子」。表示在讀取時意外抵達檔案結尾。
- MalformedInputException — 「孫子」:繼承 CharacterCodingException,而它又繼承 IOException。當檔案無法用指定的編碼正確解讀(例如預期 "UTF-8",卻收到毀損的位元序列)時會發生。
- 此外還有 SocketException、ZipException 等專門的「親戚」,各自負責不同領域。越往階層深處,情境越狹窄越專一。
一個簡化的示意圖:
java.lang.Exception
└── java.io.IOException
├── java.io.FileNotFoundException
├── java.io.EOFException
├── java.nio.charset.MalformedInputException
└── ... (其他)
有趣的事實:
在 Java 中,幾乎所有與檔案相關的操作都必須宣告或處理 IOException —— 這就是所謂的受檢例外。編譯器不會讓你忘記處理錯誤!
2. 這些例外何時、為何發生
開啟不存在的檔案
最常見的情況:你嘗試開啟檔案,但它不存在。就像到站牌等車,結果那班車根本不在時刻表上。
FileInputStream fis = new FileInputStream("abracadabra.txt"); // 砰!FileNotFoundException
在沒有權限的狀況下寫入檔案
如果你嘗試把檔案寫到沒有權限的資料夾,可能會得到 FileNotFoundException 或 IOException(視情況而定)。
FileOutputStream fos = new FileOutputStream("/system/secret.txt"); // 砰!FileNotFoundException 或 IOException
因為媒體損壞導致的讀寫錯誤
有時檔案看似存在,但磁碟受損、檔案被其他程式占用,或突然停電——此時你會收到帶有不同訊息的 IOException。
其他原因
- 檔案是唯讀,但你嘗試寫入。
- 路徑太長或包含不允許的字元。
- 檔案被其他程序使用中。
- 磁碟空間不足。
- 在檢查與實際使用之間,檔案被其他程序刪除。
3. 使用 try-catch 處理
每當你操作檔案、串流、網路——請使用 try-catch。這就像安全氣囊:若發生問題,程式不會直接當掉,而能妥善回應。
如何正確捕捉例外?
在 Java 中,較具體的例外要先捕捉,再來才是通用的。如果先放一個通用的 catch(IOException e),較狹窄的類型(例如 FileNotFoundException)就不會觸發——控制流程到不了它們。
正確的結構:
try {
// 檔案操作
} catch (FileNotFoundException e) {
// 處理「檔案未找到」的情況
} catch (IOException e) {
// 處理其他 I/O 錯誤
}
為什麼要這樣?
因為 FileNotFoundException 是 IOException 的特例。如果你先捕捉通用情況,特例就不會「走到」它自己的 catch。
範例:開啟檔案時的錯誤處理
import java.io.*;
public class FileReaderExample {
public static void main(String[] args) {
String filename = "notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line = reader.readLine();
System.out.println("檔案的第一行: " + line);
reader.close();
} catch (FileNotFoundException e) {
System.out.println("找不到檔案: " + filename);
} catch (IOException e) {
System.out.println("讀取檔案時發生錯誤: " + e.getMessage());
}
}
}
請注意:
即使你已經檢查檔案存在,也務必保留 try-catch——檔案可能隨時消失(例如被其他程序刪除)。
4. 實作:撰寫帶有錯誤處理的程式碼
我們來做幾個簡單例子,讓它們能正確應對檔案缺失與其他錯誤。
步驟 1:嘗試開啟不存在的檔案
import java.io.*;
public class NotesApp {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(filename));
String line;
System.out.println("您的筆記:");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} catch (FileNotFoundException e) {
System.out.println("哎呀!找不到筆記檔案: " + filename);
System.out.println("建議:建立檔案或檢查檔名。");
} catch (IOException e) {
System.out.println("讀取檔案時發生錯誤: " + e.getMessage());
}
}
}
步驟 2:加入寫入檔案的處理
import java.io.*;
public class NotesWriter {
public static void main(String[] args) {
String filename = "my_notes.txt";
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(filename, true)); // append: true
writer.write("新的筆記!\n");
writer.close();
System.out.println("成功新增筆記!");
} catch (IOException e) {
System.out.println("寫入檔案時發生錯誤: " + e.getMessage());
}
}
}
步驟 3:通用範例與錯誤紀錄
import java.io.*;
public class SafeFileCopier {
public static void main(String[] args) {
String source = "source.txt";
String target = "target.txt";
try {
BufferedReader reader = new BufferedReader(new FileReader(source));
BufferedWriter writer = new BufferedWriter(new FileWriter(target));
String line;
while ((line = reader.readLine()) != null) {
writer.write(line);
writer.newLine();
}
reader.close();
writer.close();
System.out.println("檔案複製成功!");
} catch (FileNotFoundException e) {
System.out.println("找不到檔案: " + e.getMessage());
} catch (IOException e) {
System.out.println("輸入/輸出錯誤: " + e.getMessage());
}
}
}
建議:
在真實應用中,錯誤不只要輸出到畫面,也應寫入檔案或日誌,以便事後追查發生了什麼問題。
5. 表格:主要的 I/O 例外
| 例外 | 何時發生? | 如何處理? |
|---|---|---|
|
找不到檔案、路徑不存在、沒有權限 | 通知使用者、檢查路徑/權限,必要時建立檔案 |
|
讀取時意外抵達檔案結尾 | 告知可能毀損/不完整,嘗試部分還原資料 |
|
一般 I/O 錯誤(磁碟、權限、鎖定) | 檢查細節、妥善結束作業,若可行則重試 |
|
編碼或檔案結構不正確 | 告知可能毀損,嘗試其他編碼/來源 |
6. 處理 I/O 例外時的常見錯誤
錯誤 1:只捕捉通用的 Exception。 很容易想直接寫 catch (Exception e),但這樣你無法分辨究竟出了什麼問題。最好先捕捉具體的例外(FileNotFoundException),再捕捉通用的 IOException。
錯誤 2:發生錯誤時未關閉串流。 如果你開了檔案,之後又拋出例外,串流可能保持未關閉。請使用 try-with-resources,或在 finally 中關閉資源。
錯誤 3:忽略例外訊息。 不要只印出「錯誤!」,請顯示詳細資訊:e.getMessage()。這能更快判斷問題所在。
錯誤 4:寫入時未處理 FileNotFoundException。 很多人以為 FileNotFoundException 只與讀取有關。其實在寫入時也可能發生(路徑不正確、沒有建立檔案的權限等)。
錯誤 5:不檢查存取權限。 如果程式以受限權限執行,許多檔案操作都可能失敗。務必考量這點並提醒使用者。
GO TO FULL VERSION