1. 損壞檔案的徵兆
在理想的世界裡,檔案總能無錯地讀寫,其中的資料就像剛出爐的麵包:柔軟、香氣四溢、整齊、完好。但在現實中,檔案可能會「壞掉」、「走樣」、「只寫了一半」或「格式不對」。原因可能很多,例如在寫入磁碟時發生故障(突然斷電)、透過網路傳輸時的錯誤、儲存媒體損壞(那個老掉牙的 bad sector)。或者,用不合適的程式手動編輯檔案也會損壞檔案,還有預期的資料格式與實際格式不一致等情形。
在 Java 中,這些情況通常會在讀取/寫入時以例外的形式表現,有時也會透過程式的怪異行為顯現(例如資料突然結束,或出現一堆亂碼)。
讀取時的例外
最明顯的跡象是非預期的例外。以下是一些常見情況:
- EOFException — 檔案意外結束(End Of File)。你預期檔案中還有資料,但實際上沒有。
- MalformedInputException(或在較舊的 API 中,來自 NIO 的 MalformedInputException)— 檔案不符合預期的編碼或結構。
- ZipException — 當你把壓縮檔當一般檔案讀取時。
- StreamCorruptedException — 讀取序列化物件時,若檔案已損壞。
資料格式不相符
有時檔案讀取時不會丟出例外,但內容並不符合預期的格式:
- 預期是一行字串,結果得到一堆難以辨識的符號(亂碼)。
- 預期有特定數量的數字,但實際上比較少。
- 預期是 CSV 檔,結果是 JSON(或反之亦然)。
實際範例
假設你寫了一個應用程式,用文字檔儲存任務清單。程式假定每一行就是一個任務。但使用者把檔案用 Excel 開啟、修改,並以另一種格式儲存……結果你的程式就讀不出這個檔案了。
2. 處理損壞檔案的策略
記錄日誌並告知使用者
第一條規則:別恐慌!(也別讓使用者恐慌)。請務必記錄錯誤,並在發生問題時通知使用者。但沒必要把 Java 堆疊追蹤的所有恐怖細節都展示給他。
try {
// 讀取檔案
} catch (EOFException e) {
System.err.println("檔案意外結束。可能已損壞。");
// 記錄詳細資訊
e.printStackTrace();
}
嘗試部分復原
有時至少可以「救回」部分資料。比如逐行讀取時,可以處理第一個錯誤之前的所有行。
使用備份(backup)
嚴謹的程式常會在寫入前替重要檔案建立備份。如果主要檔案損壞,可以嘗試從備份復原資料。
3. 實作:讀取意外結尾(EOF)的檔案
典型情況
假設有一個二進位檔,按順序寫入 int 整數。程式預期剛好有 5 個,但檔案已損壞,實際只寫了 3 個。
import java.io.*;
public class DamagedFileExample {
public static void main(String[] args) {
String filename = "numbers.bin";
// 作為示例:建立只含 3 個數字的檔案(本該是 5 個)
try (DataOutputStream out = new DataOutputStream(new FileOutputStream(filename))) {
out.writeInt(42);
out.writeInt(7);
out.writeInt(2024);
// out.writeInt(1); out.writeInt(2); // 刻意不寫入!
} catch (IOException e) {
System.err.println("建立檔案時出錯:" + e.getMessage());
}
// 現在嘗試讀取 5 個數字
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
for (int i = 0; i < 5; i++) {
int number = in.readInt();
System.out.println("讀取到數字:" + number);
}
} catch (EOFException e) {
System.err.println("檔案意外結束!可能已損壞。");
} catch (IOException e) {
System.err.println("讀取錯誤:" + e.getMessage());
}
}
}
輸出:
讀取到數字: 42
讀取到數字: 7
讀取到數字: 2024
檔案意外結束!可能已損壞。
讀到第一個錯誤為止
常見的做法是在迴圈中持續讀取,直到拋出例外為止。這樣至少能取得部分資訊。
try (DataInputStream in = new DataInputStream(new FileInputStream(filename))) {
while (true) {
try {
int number = in.readInt();
System.out.println("讀取到數字:" + number);
} catch (EOFException e) {
System.out.println("資料已結束(或檔案已損壞)。");
break;
}
}
} catch (IOException e) {
System.err.println("讀取錯誤:" + e.getMessage());
}
4. 處理文字檔與編碼
編碼問題
如果檔案用一種編碼寫入,卻用另一種編碼讀取,可能會發生解碼錯誤:
import java.nio.charset.*;
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("tasks.txt"), "UTF-8"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (MalformedInputException e) {
System.err.println("編碼錯誤!檔案已損壞或不是以 UTF-8 寫入。");
} catch (IOException e) {
System.err.println("讀取錯誤:" + e.getMessage());
}
重要:有時你不會得到例外,而是看到「亂碼」——這也是檔案損壞或編碼不正確的徵兆。
如何處理?
- 告知使用者問題所在。
- 嘗試用其他編碼開啟檔案。
- 若資料很重要——建議從備份復原。
5. 資料復原:策略
讀取部分可用的資料
如果檔案結構允許,可以先「撈出」在錯誤發生前成功讀到的資料。例如檔案是行清單(每行一個任務),就能處理所有出錯前的行。
try (BufferedReader reader = new BufferedReader(new FileReader("tasks.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 處理該行
}
} catch (IOException e) {
System.err.println("讀取錯誤:" + e.getMessage());
// 可以先保存已讀取的資料,或提示使用者進行復原
}
使用 backup 檔案
如果你事先建立了檔案副本(例如 tasks.txt.bak),就可以從中復原資料:
File original = new File("tasks.txt");
File backup = new File("tasks.txt.bak");
if (!original.exists() && backup.exists()) {
// 把備份複製回原位置
Files.copy(backup.toPath(), original.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.out.println("已從備份成功復原。");
}
雜湊檢核與驗證
對重要檔案,可以保存校驗和(例如 MD5 或 SHA-256),並在每次開啟時與最新的校驗值比對。若不一致,代表檔案已損壞。
// 範例流程(為簡化起見省略雜湊實作)
String expectedHash = "..."; // 先前保存的校驗值
String actualHash = calculateFileHash("tasks.txt");
if (!expectedHash.equals(actualHash)) {
System.out.println("檔案 tasks.txt 已損壞!請嘗試從備份復原。");
}
6. 處理損壞檔案時的常見錯誤
錯誤 №1:未檢查檔案格式。 如果你預期每一行都是例如數字,但實際上是文字,會拋出 NumberFormatException。最好在讀取的同時做驗證。
錯誤 №2:未使用 try-with-resources。 如果不使用 try-with-resources,即使出錯檔案也可能保持未關閉狀態,這會讓復原或刪除變得困難。
錯誤 №3:未先建立備份就覆寫損壞的檔案。 一旦出錯就立刻覆寫檔案,復原機率會降低。最好先保存備份。
錯誤 №4:缺乏資訊的復原流程。 應讓使用者知道檔案曾經損壞並已從備份復原——否則他可能不明白為什麼部分資料會消失。
GO TO FULL VERSION