刪除目錄及其所有內容是常見文件操作。傳統 `File.listFiles()` 遞歸存在**棧溢出風險 (深層目錄)**、**符號鏈接死循環**、**異常處理分散**、**無法處理只讀文件**等問題。JDK 7 引入 `Files.walkFileTree` 基於訪問者模式,提供**可控深度、符號鏈接感知、統一異常處理、原子操作選項**的現代解決方案。本文對比新舊方案,給出生產級代碼模板。
// 缺陷:深度遞歸棧溢出、符號鏈接無限循環、只讀文件刪除失敗無處理、異常中斷無回滾
public void deleteLegacy(File dir) {
if (dir.isDirectory()) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) deleteLegacy(file);
}
}
// 只讀文件 delete() 返回 false,不拋異常,導致殘留
dir.delete();
}
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
public final class DirectoryDeleter {
private DirectoryDeleter() {}
/**
* 刪除目錄及所有內容 (現代 NIO.2 實現)
* @param root 要刪除的根目錄路徑
* @param options 刪除選項
* @throws IOException 刪除失敗 (含權限不足、文件被佔用)
*/
public static void delete(Path root, FileVisitOption... options) throws IOException {
// 1. 選項處理:FOLLOW_LINKS 決定是否跟隨符號鏈接 (默認不跟隨,防死循環)
EnumSet opts = EnumSet.noneOf(FileVisitOption.class);
if (options != null) opts.addAll(Arrays.asList(options));
// 2. 文件訪問者:後序遍歷 (先刪子文件/子目錄,再刪父目錄)
Files.walkFileTree(root, opts, Integer.MAX_VALUE, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
// 刪除文件:嘗試修改只讀屬性後刪除
try {
Files.deleteIfExists(file); // 不存在不報錯
} catch (AccessDeniedException e) {
// Windows 只讀文件需先移除只讀屬性
if (isWindows()) {
file.toFile().setWritable(true);
Files.delete(file); // 再次嘗試
} else {
throw e;
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) throw exc; // 子遍歷拋出的異常向上傳播
Files.deleteIfExists(dir); // 刪除空目錄
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
// 訪問文件失敗 (如權限不足),決定是繼續還是拋出
// 生產環境建議記錄日誌後繼續,或拋出終止
System.err.println("無法訪問: " + file + " - " + exc.getMessage());
return FileVisitResult.CONTINUE; // 或 TERMINATE
}
});
}
// 便捷重載
public static void delete(String path) throws IOException {
delete(Paths.get(path));
}
public static void delete(Path path) throws IOException {
delete(path, (FileVisitOption[]) null);
}
private static boolean isWindows() {
return System.getProperty("os.name").toLowerCase().contains("win");
}
}
| 特性 | Files.walkFileTree | 傳統遞歸 File.listFiles() |
|---|---|---|
| 棧溢出風險 | 無 (內部迭代,可限制深度 `maxDepth`) | 高 (深層目錄超過棧幀限制 ~10k) |
| 符號鏈接處理 | 可控 (`FOLLOW_LINKS` 選項,默認不跟隨) | 無感知,極易進入無限循環 |
| 只讀文件刪除 | 可在 `visitFile` 中處理屬性修改後重試 | 靜默失敗 (`delete()` 返回 false),難以察覺 |
| 異常處理 | 統一在 `visitFileFailed` / `postVisitDirectory` 捕獲決策 | 分散在各調用點,易遺漏 |
| 原子性/一致性 | 單次遍歷完成,中間狀態可見但可控 | 無保證,異常中斷留下殘留 |
| 效能 | 原生系統調用,批量屬性讀取 | 多次 JNI 調用,逐個 `listFiles()` 開銷大 |
public static void deleteParallel(Path root, int parallelism) throws IOException {
// 1. 先收集頂層子目錄 (淺層遍歷)
List subDirs;
try (Stream stream = Files.list(root)) {
subDirs = stream.filter(Files::isDirectory).toList();
}
// 2. 並行刪除各子樹
try (var executor = Executors.newFixedThreadPool(parallelism)) {
List> futures = subDirs.stream()
.map(dir -> CompletableFuture.runAsync(() -> {
try { delete(dir); } catch (IOException e) { throw new CompletionException(e); }
}, executor))
.toList();
// 等待全部完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
// 3. 最後刪除根目錄 (此刻應為空)
Files.deleteIfExists(root);
}
// 不直接刪除,移動到 .trash/{timestamp}/{original_path}
public static void moveToTrash(Path file) throws IOException {
Path trashRoot = Paths.get(System.getProperty("user.home"), ".trash",
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")));
Path relative = file.getFileSystem().getPath("/").relativize(file.toAbsolutePath()); // 簡化
Path target = trashRoot.resolve(relative);
Files.createDirectories(target.getParent());
Files.move(file, target, StandardCopyOption.ATOMIC_MOVE);
}
| 錯誤 | 後果 | 修正 |
|---|---|---|
| `Files.walkFileTree(root, visitor)` 不指定 `maxDepth` | 極深目錄可能耗盡資源 (雖不棧溢出,但訪問者對象持有上下文) | 傳遞 `Integer.MAX_VALUE` 或業務合理深度 (如 1000) |
| 刪除前未檢查 `Files.isDirectory(root)` | 傳入文件路徑拋 `NotDirectoryException` | 調用前 `if (!Files.isDirectory(root)) throw new IllegalArgumentException();` |
| Windows 下刪除被佔用文件 | `AccessDeniedException` / `FileSystemException` | 捕獲異常,記錄日誌,嘗試 `File.deleteOnExit()` 或排程稍後重試 |
| 跟隨符號鏈接 `FOLLOW_LINKS` 刪除 | 可能誤刪鏈接目標的真實數據,甚至跨文件系統刪除關鍵數據 | **默認不跟隨**;若必須跟隨,極其謹慎,確認無循環 |
**拒絕手寫遞歸刪除**。`Files.walkFileTree` 是 JDK 7+ 規範的、健壯的、高效的標準方案。它通過訪問者模式實現後序遍歷,原生支援符號鏈接控制、深度限制、統一異常處理、只讀文件屬性修正。生產代碼只需複製 `DirectoryDeleter.delete(path)` 即可獲得企業級可靠性。大規模場景可結合並行流或 `CompletableFuture` 進行子樹並行刪除,並考慮回收站機制保障數據安全。