1. 在目錄中遍歷檔案
取得檔案列表:Files.list 與 Files.walk
在 Java 中,要處理資料夾內容有兩個非常好用的方法:
- Files.list(Path) — 傳回指定目錄中頂層所有檔案與資料夾的串流(Stream<Path>)。
- Files.walk(Path) — 傳回目錄及其所有子資料夾(遞迴)中的所有檔案與資料夾的串流。
範例:輸出目錄 data 中的所有檔案與資料夾
import java.nio.file.*;
import java.io.IOException;
public class ListFilesExample {
public static void main(String[] args) throws IOException {
Path dir = Paths.get("data");
// 取得所有頂層檔案與資料夾的 stream
try (var stream = Files.list(dir)) {
stream.forEach(System.out::println);
}
}
}
如果你想遍歷所有巢狀的資料夾(遞迴),請使用 Files.walk:
try (var stream = Files.walk(dir)) {
stream.forEach(System.out::println);
}
注意:
Files.walk 在結構很大時可能會返回非常多的檔案——不要在磁碟根目錄上執行,除非你想看到「整個宇宙的清單」!
可視化:list 與 walk 的差異
| 方法 | 返回內容 |
|---|---|
|
僅目前資料夾的內容 |
|
所有檔案與資料夾(包含巢狀的) |
2. 檔案篩選
使用串流(Stream<Path>)的一大優勢——可以依任何條件篩選檔案:副檔名、名稱、大小、建立/修改日期等。
依副檔名篩選
假設我們只要 .txt 檔,可以這樣過濾:
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
依名稱篩選
想要所有檔名以 "report" 開頭的檔案:
stream
.filter(path -> path.getFileName().toString().startsWith("report"))
.forEach(System.out::println);
依大小篩選
只取較大的檔案(大於 1 MB):
stream
.filter(path -> {
try {
return Files.size(path) > 1_000_000;
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
依日期篩選
例如,最近 7 天內修改的檔案:
import java.time.*;
import java.nio.file.attribute.*;
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isAfter(Instant.now().minus(Duration.ofDays(7)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
整合:組合式篩選
可以像在餐廳點菜一樣組合篩選條件:例如,所有超過一個月且大於 10 KB 的 .log 檔:
stream
.filter(path -> path.toString().endsWith(".log"))
.filter(path -> {
try {
return Files.size(path) > 10_000;
} catch (IOException e) {
return false;
}
})
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(30)));
} catch (IOException e) {
return false;
}
})
.forEach(System.out::println);
3. 批次複製與刪除檔案
批次操作很簡單:先取得檔案的串流,接著對每個元素套用需要的函式。重點是別忘了可能發生的錯誤(例如檔案被占用或目標已存在)。
將一個資料夾中的所有檔案複製到另一個資料夾
import java.nio.file.StandardCopyOption;
Path sourceDir = Paths.get("data");
Path targetDir = Paths.get("backup");
try (var stream = Files.list(sourceDir)) {
stream.forEach(path -> {
Path targetPath = targetDir.resolve(path.getFileName());
try {
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("已複製: " + path.getFileName());
} catch (IOException e) {
System.out.println("複製錯誤 " + path.getFileName() + ": " + e.getMessage());
}
});
}
刪除所有暫存檔(*.tmp)
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已刪除: " + path.getFileName());
} catch (IOException e) {
System.out.println("刪除錯誤 " + path.getFileName() + ": " + e.getMessage());
}
});
}
使用 Stream API 處理路徑集合
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.sorted((p1, p2) -> {
try {
return Long.compare(Files.size(p2), Files.size(p1)); // 依大小,遞減
} catch (IOException e) {
return 0;
}
})
.limit(5) // 只取最大的 5 個
.forEach(System.out::println);
}
4. 實務練習
查找並刪除早於特定日期的檔案
刪除一年以上未修改的所有檔案:
try (var stream = Files.list(dir)) {
stream
.filter(path -> {
try {
FileTime lastModified = Files.getLastModifiedTime(path);
return lastModified.toInstant().isBefore(Instant.now().minus(Duration.ofDays(365)));
} catch (IOException e) {
return false;
}
})
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已刪除: " + path.getFileName());
} catch (IOException e) {
System.out.println("刪除錯誤 " + path.getFileName() + ": " + e.getMessage());
}
});
}
依樣板批次重新命名檔案
假設要為每個 .txt 檔加上前綴 "old_":
try (var stream = Files.list(dir)) {
stream
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
Path newPath = path.resolveSibling("old_" + path.getFileName());
try {
Files.move(path, newPath);
System.out.println("已重新命名: " + path.getFileName() + " -> " + newPath.getFileName());
} catch (IOException e) {
System.out.println("重新命名錯誤 " + path.getFileName() + ": " + e.getMessage());
}
});
}
複製所有子資料夾中的所有檔案(遞迴)
使用 Files.walk 來遍歷所有子資料夾中的檔案:
try (var stream = Files.walk(sourceDir)) {
stream
.filter(Files::isRegularFile)
.forEach(path -> {
Path relative = sourceDir.relativize(path);
Path targetPath = targetDir.resolve(relative);
try {
Files.createDirectories(targetPath.getParent());
Files.copy(path, targetPath, StandardCopyOption.REPLACE_EXISTING);
System.out.println("已複製: " + path + " -> " + targetPath);
} catch (IOException e) {
System.out.println("複製錯誤 " + path + ": " + e.getMessage());
}
});
}
5. 重要注意事項與細節
批次操作中的錯誤處理
在批次操作中幾乎一定會遇到「有問題的」檔案:沒有存取權、檔案被占用、路徑太長、檔案已存在等。如果不在迴圈內處理例外,程式可能在第一個失敗處就「倒下」。因此請務必在 forEach 內使用 try-catch,對單一檔案的錯誤加以「吞掉」,讓其他檔案仍能繼續處理。
遞迴對效能與呼叫堆疊的影響
在大型目錄上使用 Files.walk 時,如果手動撰寫遞迴很容易讓遍歷路徑變得很長。所幸該方法本身實作有效率,不會造成堆疊溢位。不過如果你自己寫遞迴函式來刪除/複製目錄,請務必小心:對於極度巢狀的結構,呼叫堆疊可能「撐不住」。
處理大型資料夾
- 謹慎使用 Files.walk:如果資料夾內有成千上萬個檔案,可能會花費時間並佔用大量記憶體。
- 若只需處理最上層——請使用 Files.list。
- 要依樣式尋找檔案,請使用名稱篩選(endsWith、matches 等)。
範例:「清理資料夾中的暫存檔」
Path tempDir = Paths.get("data/tmp");
try (var stream = Files.walk(tempDir)) {
stream
.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".tmp"))
.forEach(path -> {
try {
Files.delete(path);
System.out.println("已刪除: " + path);
} catch (IOException e) {
System.out.println("刪除錯誤 " + path + ": " + e.getMessage());
}
});
}
批次操作的主要方法
| 操作 | 方法/做法 | 特點 |
|---|---|---|
| 取得所有檔案 | |
walk — 遞迴,list — 只有最上層 |
| 依副檔名篩選 | |
可與其他篩選條件組合 |
| 複製一組檔案 | |
請確認目標資料夾已存在 |
| 刪除一組檔案 | |
最好為每個操作使用 try-catch |
| 批次重新命名 | |
重新命名就是以新名稱執行 move |
6. 批次操作中的常見錯誤
錯誤 №1: 在 forEach 內未處理的例外。
非常常見:如果不將每個操作包在 try-catch 中,程式會在第一個有問題的檔案就「當掉」,其餘將不會被處理。
錯誤 №2: 嘗試把資料夾當成檔案來刪除/複製(或反之)。
方法 Files.delete 與 Files.copy 對檔案與資料夾的處理方式不同,別搞混!例如,嘗試用標準方法刪除非空的資料夾會拋出錯誤。
錯誤 №3: 複製時錯誤地建立目標路徑。
如果在建立目標路徑時沒有考慮結構(例如沒有使用 relativize),可能會覆寫檔案,或在封裝時得到錯誤的目錄結構。
錯誤 №4: 同時開啟過多檔案。
如果你在迴圈內開啟讀寫串流,請記得關閉!最好使用 try-with-resources。
錯誤 №5: 遞迴遍歷未限制深度。
在非常大且很深的資料夾上使用 Files.walk 可能會造成效能與記憶體問題。如果需要限制深度——請使用帶有深度參數的多載版本:Files.walk(dir, depth)。
錯誤 №6: 不小心處理到隱藏/系統檔案。
如果應用程式處理的是使用者檔案,可能會不小心動到隱藏或系統檔案。要過濾它們,請使用方法 Files.isHidden(path)。
GO TO FULL VERSION