1. NIO2: 認識更多細節
我們已經接觸過 NIO2,現在來複習並加深對這個用於檔案與目錄操作的實用程式庫的理解。
早期在 Java 中只有 File 類。它能檢查檔案是否存在、建立與刪除檔案/資料夾、取得目錄中的檔案列表。但它有許多限制:
- 處理路徑不方便,特別是當需要考慮不同作業系統時(Windows 上的 C:\Users\user\file.txt 與 Linux 上的 /home/user/file.txt);
- 缺乏對符號連結、存取權限與檔案屬性的完善支援;
- 遍歷目錄樹的能力有限;
- 錯誤處理不夠理想。
隨著 Java 7 引入 NIO2(New Input/Output,版本 2),開發者的生活變得更輕鬆。現在我們可以使用:
- Path 類來更便利地處理檔案與資料夾的路徑;
- Files 類,提供所有核心操作:讀取、寫入、複製、刪除、取得檔案資訊;
- FileVisitor 介面與像 Files.walk 這樣的方法,讓檔案系統遍歷變得容易且彈性十足。
為什麼這很重要?
- 跨平台:同一份程式碼能在 Windows、Linux、macOS 上運作,無需在意分隔符(/ 或 \)。
- 安全與易用:更豐富的錯誤資訊,更少的黑魔法與意外驚喜。
- 強大:能處理巨大的目錄,並可用篩選與平行處理進行遞迴遍歷。
2. 核心類別:Path 與 Files
類別 Path
Path 是現代的檔案或資料夾路徑表示。它不一定指向實際存在的檔案——只是個方便操作的路徑。
取得 Path
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // 相對路徑
Path path2 = Paths.get("/home/user/file.txt"); // 絕對路徑
Path path3 = Path.of("mydir", "subdir", "file.txt"); // 從 Java 11+ 起
事實:Path 與作業系統無關。忘掉手動用 / 或 \ 串接字串吧!
轉為字串
System.out.println(path1.toString());
取得父目錄與檔名
Path parent = path1.getParent(); // 對於相對路徑可能為 null
Path fileName = path1.getFileName(); // 只有檔名
類別 Files
Files 是一組針對檔案與目錄的靜態方法:
- 檢查是否存在:Files.exists(path)
- 讀寫檔案:Files.readAllBytes(path)、Files.write(path, bytes)
- 取得資訊:Files.size(path)、Files.getLastModifiedTime(path)
- 複製、刪除、移動:Files.copy、Files.delete、Files.move
範例:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("檔案存在!");
System.out.println("大小: " + Files.size(path) + " 位元組");
System.out.println("最後修改時間: " + Files.getLastModifiedTime(path));
} else {
System.out.println("找不到檔案。");
}
3. 檔案系統遍歷:Files.walk 與夥伴們
舊方法的痛點
在舊的 API 中,要遍歷資料夾及其子資料夾內的所有檔案,必須自己寫遞迴函式、手動判斷是檔案還是資料夾,還要避免陷入無限遞迴。這不僅繁瑣,也非常容易出錯。
現代方法:Files.walk
Files.walk(Path start) 會回傳 Stream<Path>——從指定路徑開始的所有檔案與資料夾(包含所有子目錄)。現在遍歷檔案系統就是在使用 Stream!
範例:列出所有檔案與資料夾
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
這裡會列出所有路徑:包含檔案與資料夾,從 mydir 開始。
範例:只要檔案(不包含資料夾)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
Files.isRegularFile(path) 只會對一般檔案回傳 true(不包含資料夾與符號連結)。
範例:依副檔名搜尋檔案
假設我們需要在目錄與子目錄中找到所有 .txt 檔案:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
範例:計算所有檔案的總大小
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("讀取大小時發生錯誤: " + path);
return 0L;
}
})
.sum();
}
System.out.println("檔案總大小: " + totalSize + " 位元組");
重要!
- Files.walk 回傳的是需要關閉的串流(它實作了 AutoCloseable)。因此請使用 try-with-resources!
- 預設會遍歷到最深(所有子目錄)。可以限制深度:Files.walk(path, maxDepth)
4. 實作練習
任務 1:找出目錄中的所有圖片
需要在 images 資料夾中找出副檔名為 .jpg、.png、.gif 的所有檔案並列出其名稱。
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
任務 2:將所有 .txt 檔案複製到另一個資料夾
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("已複製: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("複製錯誤: " + path);
}
});
}
這裡同時保留了子目錄的結構。
5. 有用的細節
NIO2 的優勢
跨平台性
Path 會自行處理資料夾分隔符。你的程式碼能在 Windows、Linux、macOS 上一致地運作。
串流式處理
像 Files.walk 這樣的方法會回傳串流(Stream<Path>),你可以過濾、轉換、收集成集合——Stream API 能做的都可以。
處理大型目錄
舊 API 在檔案非常多時可能會崩潰(例如 100 000 張照片)。NIO2 能輕鬆應對,因為它不會一次把所有內容載入記憶體。
支援符號連結、屬性、存取權限
你可以判斷路徑是否為符號連結(Files.isSymbolicLink(path))、取得存取權限(Files.getPosixFilePermissions(path))、查詢檔案擁有者等等。
舊 API 與新 API 的比較
| 操作 | 舊 API(File) | 新 API(Path、Files) |
|---|---|---|
| 檢查是否存在 | |
|
| 取得大小 | |
|
| 資料夾中的檔案列表 | |
|
| 遞迴遍歷 | 手動遞迴 | |
| 複製檔案 | file.renameTo()(不可靠) | |
| 取得副檔名 | 手動解析字串 | |
| 取得父層 | |
|
| 權限處理 | 幾乎沒有 | |
重要特性
檔案型態檢查
- Files.isRegularFile(path) — 一般檔案
- Files.isDirectory(path) — 目錄
- Files.isSymbolicLink(path) — 符號連結
處理大型目錄
- 不要把所有路徑收集成清單:請使用串流(Stream<Path>)並邊走邊處理。
- 完成後務必關閉串流(try-with-resources)。
例外
- 幾乎所有方法都可能丟出 IOException——請記得處理錯誤(或往上拋出)。
限制遍歷深度
try (var paths = Files.walk(Path.of("mydir"), 2)) { // 只到 2 層
// ...
}
6. 使用 NIO2 時的常見錯誤
錯誤 №1:忘了關閉 walk 的串流。 如果不使用 try-with-resources,可能會造成資源洩漏——檔案系統的串流保持開啟。請務必使用 try (var paths = Files.walk(...)) { ... } 結構。
錯誤 №2:沒有先確認路徑是目錄。 若把檔案路徑而非資料夾傳給 Files.walk,可能導致非預期行為或錯誤。
錯誤 №3:沒有處理例外。 幾乎所有 NIO2 方法都可能丟出 IOException。不要忽略這些錯誤——至少要顯示訊息或記錄日誌。
錯誤 №4:對路徑分隔符感到混亂。 如果你還在手動用 / 或 \ 串接路徑,那就多此一舉了!請使用 Path.of(...) 或 resolve(...)——它們會自動處理好各種情況。
錯誤 №5:嘗試把巨大目錄一次載入到記憶體。 當檔案很多時,不要把所有路徑收集成清單——請使用串流(Stream<Path>)並依序處理。
錯誤 №6:忽略跨平台性。 不要硬編寫 Windows 或 Unix 風格的絕對路徑。請使用 Path 與 resolve/relativize 等操作——它們在任何作業系統上都能正確處理。
GO TO FULL VERSION