1. 簡介
在處理檔案與資料夾時,經常需要只挑出特定檔案:例如所有 ".java" 檔、所有圖片、某一天的所有日誌。為此,在 Java(以及不只 Java)會使用 globbing(glob)與 正規表示式(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 的全部能力 |
| 範例 | |
|
| 遞迴能力 | |
無內建支援 |
| 何時使用 | 檔案篩選 | 複雜的檢查 |
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.find 與 Files.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: 忘了關閉 DirectoryStream 或 Stream<Path> — 會導致資源外洩。
錯誤 4: 為簡單篩選撰寫過度複雜的 regex 樣式 — 若 glob 足夠就用它。
錯誤 5: 未排除系統資料夾(".git"、"node_modules")— 搜尋會變慢且被無關檔案「汙染」。
GO TO FULL VERSION