CodeGym /課程 /JAVA 25 SELF /Globbing/PathMatcher, DirectoryStream.Filter

Globbing/PathMatcher, DirectoryStream.Filter

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

1. 簡介

在處理檔案與資料夾時,經常需要只挑出特定檔案:例如所有 ".java" 檔、所有圖片、某一天的所有日誌。為此,在 Java(以及不只 Java)會使用 globbingglob)與 正規表示式regex)。

  • Globbing — 一種用特殊字元描述檔名樣式的簡單方法(*?[]{}),類似於 Linux 或 Windows 命令列。
  • Regex — 用於複雜樣式的強大正規表示式語言。

Globbing 樣式範例

  • *.java — 目前資料夾中的所有 ".java" 副檔名檔案。
  • **/*.java — 所有子資料夾中的 ".java" 檔(雙星號 — 遞迴搜尋)。
  • *.{png,jpg} — 副檔名為 ".png"".jpg" 的所有檔案。
  • file-??.log — 像是 "file-01.log""file-AB.log"(兩個任意字元)。
  • [A-Z]*.txt — 以大寫字母開頭的所有 ".txt" 檔案。

Globbing 比 regex 更簡單,且在多數情境下足以進行檔案篩選。

glob 與 regex 的比較

特性 glob regex
易用性 非常簡單 較複雜但更強大
可用符號
*, ?, [], {}
regex 的全部能力
範例
*.java
.*\.java
遞迴能力
**/*.java
無內建支援
何時使用 檔案篩選 複雜的檢查

2. PathMatcher:依樣式篩選檔案

在 Java NIO(java.nio.file)中,會使用介面 PathMatcher 來依樣式篩選檔案。可以透過 FileSystems.getDefault().getPathMatcher(...) 取得。

語法

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");
  • "glob:*.java" — glob 樣式。
  • "regex:.*\\.java" — 正規表示式樣式。

範例:篩選資料夾中的檔案

import java.nio.file.*;

Path dir = Paths.get("src");
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:*.java");

try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
    for (Path entry : stream) {
        if (matcher.matches(entry.getFileName())) {
            System.out.println(entry);
        }
    }
}

重點:

  • matcher.matches() 通常只檢查檔名 — 請傳入 entry.getFileName(),而非完整路徑。
  • 若要遞迴搜尋,請使用 Files.walk()Files.find()

Files.walk() 是 NIO2 的方法,會回傳包含指定目錄下所有檔案與資料夾的 Stream<Path>,且會遞迴包含子資料夾。 與只顯示單一資料夾內容的 DirectoryStream 不同,Files.walk() 允許你透過 Stream API 來處理整個目錄樹。

使用範例:

import java.nio.file.*;
import java.util.stream.Stream;

Path start = Paths.get("src");
try (Stream<Path> stream = Files.walk(start)) { // 遞迴包含所有子資料夾
    stream.filter(Files::isRegularFile)
          .forEach(System.out::println);
}

搭配 regex 的範例

PathMatcher matcher = FileSystems.getDefault().getPathMatcher("regex:.*\\.(png|jpg)");

3. Files.newDirectoryStream:在瀏覽資料夾時進行篩選

方法 Files.newDirectoryStream() 可以直接依樣式或自訂過濾器來篩選檔案。

以 glob 樣式篩選

try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
    for (Path entry : stream) {
        System.out.println(entry);
    }
}

第二個參數是 glob 樣式(不需加上前綴 "glob:")。

使用 DirectoryStream.Filter 篩選

若需要更複雜的邏輯(例如依大小、日期、排除資料夾),請使用 DirectoryStream.Filter<Path>

DirectoryStream.Filter<Path> filter = path -> {
    // 範例:只要檔案(非資料夾),且排除 .git 與 node_modules
    return Files.isRegularFile(path)
        && !path.getFileName().toString().equals(".git")
        && !path.getFileName().toString().equals("node_modules");
};

try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
    for (Path entry : stream) {
        System.out.println(entry);
    }
}

4. Files.find:搭配 BiPredicate 的強大查找

若要依複雜條件(例如日期、大小、名稱,且需要遞迴)搜尋檔案,請使用 Files.find

語法

Stream<Path> stream = Files.find(
    startDir, // 搜尋起點
    maxDepth, // 深度(Integer.MAX_VALUE — 遞迴)
    (path, attrs) -> {
        // path — 檔案的路徑
        // attrs — 檔案屬性(大小、日期等)
        return path.getFileName().toString().endsWith(".log")
            && attrs.size() > 1024; // 只要較大的日誌
    }
);

stream.forEach(System.out::println);
  • 第二個參數 — 最大搜尋深度。
  • 第三個參數 — BiPredicate<Path, BasicFileAttributes>:當檔案符合條件時回傳 true

範例:排除資料夾 .git 與 node_modules

Stream<Path> stream = Files.find(
    Paths.get("."),
    Integer.MAX_VALUE,
    (path, attrs) -> {
        String name = path.getFileName().toString();
        // 排除 .git 與 node_modules 資料夾
        if (name.equals(".git") || name.equals("node_modules")) return false;
        // 只要大於 1 MB 的 .log 檔
        return name.endsWith(".log") && attrs.size() > 1024 * 1024;
    }
);
stream.forEach(System.out::println);

5. 實作練習

範例 1:找出所有 .log 檔,排除 .git 與 node_modules

Files.walk(Paths.get("."))
    .filter(path -> {
        String name = path.getFileName().toString();
        // 排除資料夾
        if (name.equals(".git") || name.equals("node_modules")) return false;
        // 僅限 .log 檔
        return name.endsWith(".log");
    })
    .forEach(System.out::println);

範例 2:找出在特定日期之後建立的所有圖片

import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;

Instant after = Instant.parse("2024-06-01T00:00:00Z");

Files.find(
    Paths.get("images"),
    Integer.MAX_VALUE,
    (path, attrs) -> {
        String name = path.getFileName().toString();
        return (name.endsWith(".png") || name.endsWith(".jpg"))
            && attrs.creationTime().toInstant().isAfter(after);
    }
).forEach(System.out::println);

範例 3:找出所有大檔(大於 10 MB),排除 .git

Files.find(
    Paths.get("."),
    Integer.MAX_VALUE,
    (path, attrs) -> {
        String name = path.getFileName().toString();
        return !name.equals(".git") && attrs.size() > 10 * 1024 * 1024;
    }
).forEach(System.out::println);

6. 實用細節

glob 語法速查

  • * — 任意數量的任意字元(不含分隔符 /
  • ** — 任意層級的資料夾(在 Java 中可用)
  • ? — 恰好一個任意字元
  • [abc] — a、b、c 之中的任一字元
  • [a-z] — 指定範圍內的任一字元
  • {a,b,c} — 列舉中的任一值(例如 *.{png,jpg}

範例:

  • *.java — 目前資料夾中的所有 ".java"
  • **/*.java — 所有子資料夾中的 ".java"
  • file-??.log — 例如 "file-01.log""file-AB.log"
  • [A-Z]*.txt — 以大寫字母開頭的所有 ".txt" 檔案

效能與資源外洩:請務必關閉 DirectoryStream!

重要!

  • DirectoryStream 以及由 Files.find/Files.walk 產生的串流都屬於需要關閉的資源。
  • 請使用 try-with-resources
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.java")) {
    for (Path entry : stream) {
        // ...
    }
}
  • 若未關閉串流,可能發生資源外洩(例如「開啟的檔案過多」)。
  • 對於 Files.findFiles.walk — 務必呼叫 close() 或使用 try-with-resources:
try (Stream<Path> stream = Files.find(...)) {
    stream.forEach(System.out::println);
}

7. 總結與常見錯誤

錯誤 1: 使用 glob 樣式卻不知道 * 不會做遞迴搜尋。若需遞迴請使用 "**/*.java"Files.walk

錯誤 2: 將完整路徑傳給 matcher.matches() — 通常只需傳入檔名(getFileName())。

錯誤 3: 忘了關閉 DirectoryStreamStream<Path> — 會導致資源外洩。

錯誤 4: 為簡單篩選撰寫過度複雜的 regex 樣式 — 若 glob 足夠就用它。

錯誤 5: 未排除系統資料夾(".git""node_modules")— 搜尋會變慢且被無關檔案「汙染」。

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