1. 錯誤處理:不要忽略例外!
與檔案打交道本質上是在和外部世界互動,而外部世界常常難以預測。磁碟可能爆滿,檔案可能消失,權限可能變更,用戶也可能做出超乎想像的操作(例如從資料夾名稱含有空白的路徑執行你的程式,或從隨時可能被拔掉的隨身碟執行)。如果你的程式碼沒有為此做好準備,它不僅可能「崩潰」,還可能讓使用者丟失關鍵資料。
最佳實務不只是「時髦的建議」,而是一套經得起時間考驗的方法,能幫你避開最糟糕的情況:資料遺失、記憶體洩漏、敏感資訊外洩,以及那些讓你之後難以面對同事(更別說使用者)的愚蠢 bug。
為什麼不能寫空的 catch?
在 Java(以及其他語言)中,很容易想寫出類似這樣的程式碼:
try {
// 處理檔案
} catch (IOException e) {
// 沒成功就算了!
}
這是最糟的做法。這樣的程式碼不只是「吞掉」錯誤,還會讓錯誤對使用者與你自己都變得不可見。結果就是,一旦出了問題,你永遠不知道何時、到底哪裡出了狀況。
正確的作法是?
- 記錄錯誤:至少在主控台輸出訊息,或寫入日誌檔。
- 通知使用者:若錯誤屬於嚴重問題,顯示友善的提示訊息。
- 不要洩露過多資訊:不要向使用者顯示系統內部細節(例如完整的 stack trace——那是給開發者看的)。
範例:
try {
List<String> lines = Files.readAllLines(Path.of("data.txt"));
// 資料處理
} catch (IOException e) {
System.err.println("讀取檔案時發生錯誤:" + e.getMessage());
// 可將詳細資訊寫入日誌檔
e.printStackTrace(System.err);
}
為什麼要攔截具體的例外?
因為不同的錯誤需要不同的處理。例如,找不到檔案時,可以請使用者選擇其他檔案(NoSuchFileException 或 FileNotFoundException)。如果沒有權限,請使用者以足夠權限啟動程式(AccessDeniedException)。若磁碟已滿,提示釋放空間(寫入時的 IOException)。
2. 存取權限與安全性
在操作前先檢查存取權限
在讀寫檔案之前,最好先確認你具有相應的權限。在 Java 中可以使用下列方法:
- File.canRead()
- File.canWrite()
即使這些方法回傳 true,也不代表一定會成功——權限隨時可能被改變(例如其他行程變更了權限)。因此務必隨時準備好處理例外。
範例:
File file = new File("config.properties");
if (!file.canRead()) {
System.err.println("沒有讀取檔案的權限!");
return;
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
// 讀取檔案
} catch (IOException e) {
System.err.println("讀取時發生錯誤:" + e.getMessage());
}
不要暴露內部細節
如果你的程式處理機密檔案(例如密碼),請不要在使用者可見的錯誤訊息中輸出這些檔案的路徑或其內容。
3. 關鍵操作不要使用相對路徑
相對路徑(new File("data.txt")) 是相對於目前工作目錄,而工作目錄可能會因為啟動方式不同而變化(例如從 IDE 或命令列啟動)。這可能導致混淆與錯誤。
最佳實務:對於重要檔案請使用絕對路徑,或明確指定工作目錄。
範例:
String userHome = System.getProperty("user.home");
Path configPath = Path.of(userHome, "myapp", "config.properties");
4. 使用暫存檔與暫存目錄
為什麼需要暫存檔?
暫存檔可用於各種情境。有時它們用於中間處理:例如先把資料寫入暫存檔,之後再以該檔替換主要檔案。另一種情況是,暫存檔用於保存程式結束後不再需要、可安全刪除的資訊。
如何安全地建立暫存檔?
使用 java.nio.file.Files 中的方法:
Path tempFile = Files.createTempFile("myapp_", ".tmp");
// ... 操作檔案
Files.deleteIfExists(tempFile);
暫存目錄
Path tempDir = Files.createTempDirectory("myapp_");
5. 可靠性:備份與完整性檢查
修改重要檔案前請先建立備份
在覆寫重要檔案(例如設定檔)之前,先建立一份副本:
Path config = Path.of("config.properties");
Path backup = Path.of("config.properties.bak");
if (Files.exists(config)) {
Files.copy(config, backup, StandardCopyOption.REPLACE_EXISTING);
}
如果寫入過程出現問題,隨時都能從備份還原。
檢查資料完整性
對特別重要的資料,可以使用雜湊校驗(例如 MD5 或 SHA-256)。寫入檔案後計算 checksum 並將其一併保存;讀取時再檢查檔案是否被更動。
SHA-256 計算範例(給密碼學愛好者):
import java.security.MessageDigest;
import java.nio.file.Files;
import java.nio.file.Path;
byte[] data = Files.readAllBytes(Path.of("important.dat"));
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
// 將雜湊保存到另一個檔案,或在讀取時比對
6. 將檢查與使用之間的時間窗口最小化
這就是經典的 TOCTOU(Time Of Check To Time Of Use)問題:在你檢查檔案存在與開始讀取之間,檔案可能被刪除或被修改。
因此盡量將檢查與使用放在同一個 try 區塊中。即使剛剛檢查過檔案,也一定要處理例外。
範例:
Path filePath = Path.of("data.txt");
if (Files.exists(filePath)) {
try (BufferedReader reader = Files.newBufferedReader(filePath)) {
// 讀取檔案
} catch (IOException e) {
System.err.println("讀取檔案時發生錯誤(檔案可能已消失):" + e.getMessage());
}
}
7. 其他一些實用建議
對所有資源使用 try-with-resources
所有實作了介面 AutoCloseable 的類別(幾乎所有 Java IO/NIO 串流)都可以搭配 try-with-resources 使用。這能防止資源洩漏。
try (BufferedReader reader = Files.newBufferedReader(Path.of("data.txt"))) {
// 讀取
}
別忘了刪除暫存檔
Files.deleteIfExists(tempFile);
不要重複關閉資源
若你使用了 try-with-resources,就不要再手動呼叫 close()——這可能導致錯誤以及重複的關閉嘗試。
8. 操作檔案時的常見錯誤
錯誤 №1:忽略例外。
寫空的 catch 就像徒手抓蒼蠅又把牠放回去。一定要記錄,至少也要告訴使用者發生了什麼問題。
錯誤 №2:沒有關閉串流。
如果忘了關閉串流,檔案可能保持鎖定,系統也可能耗盡可用的檔案描述符。請使用 try-with-resources。
錯誤 №3:對重要檔案使用相對路徑。
不要指望工作目錄總是你所預期的。最好明確指定路徑,或使用指定的系統目錄(user.home、java.io.tmpdir)。
錯誤 №4:覆寫重要檔案而不做備份。
在覆蓋任何重要內容前,先做 backup。這會保住你的理智與使用者的資料。
錯誤 №5:未檢查存取權限。
請確認使用者對所需的檔案或目錄具有讀寫權限——否則會遇到意料之外的 AccessDeniedException。
錯誤 №6:TOCTOU 視窗。
在檢查與使用之間,別人可能修改或刪除該檔案。即使檢查過也要一律處理例外。
錯誤 №7:遺留暫存檔與垃圾。
在程式異常終止或出錯後,暫存檔可能會殘留。別忘了清理它們,尤其是含有敏感資料時。
GO TO FULL VERSION