CodeGym /課程 /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: 檔案系統遍歷

NIO2: Files, Paths, Files.walk: 檔案系統遍歷

JAVA 25 SELF
等級 39 , 課堂 0
開放

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.copyFiles.deleteFiles.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(PathFiles
檢查是否存在
file.exists()
Files.exists(path)
取得大小
file.length()
Files.size(path)
資料夾中的檔案列表
file.listFiles()
Files.list(path)
遞迴遍歷 手動遞迴
Files.walk(path)
複製檔案 file.renameTo()(不可靠)
Files.copy(src, dest)
取得副檔名 手動解析字串
path.getFileName().toString()
取得父層
file.getParentFile()
path.getParent()
權限處理 幾乎沒有
Files.getPosixFilePermissions(path)

重要特性

檔案型態檢查

  • 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 風格的絕對路徑。請使用 Pathresolve/relativize 等操作——它們在任何作業系統上都能正確處理。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION