文件刪除看似簡單,實則暗藏陷阱:符號鏈接循環、權限不足、文件被佔用、目錄非空、路徑過長、NIO.2 原子操作等。本文提供從基礎遞歸到生產就緒的完整實現,並對比 java.io.File 與 java.nio.file.Files 兩代 API 的優劣。
import java.io.File;
import java.io.IOException;
public class FileDeleter {
/** 刪除文件或目錄(含所有子項) */
public static boolean deleteRecursively(File fileOrDir) {
if (fileOrDir == null || !fileOrDir.exists()) {
return false; // 不存在視為成功/無操作
}
if (fileOrDir.isFile()) {
return fileOrDir.delete();
}
// 目錄:先刪除所有子項
File[] children = fileOrDir.listFiles();
if (children != null) {
for (File child : children) {
if (!deleteRecursively(child)) {
System.err.println("刪除失敗: " + child.getAbsolutePath());
return false; // 遇到失敗可選擇停止或繼續
}
}
}
// 最後刪除空目錄
return fileOrDir.delete();
}
public static void main(String[] args) {
File target = new File("d:/test");
boolean ok = deleteRecursively(target);
System.out.println(ok ? "刪除完成" : "刪除過程中有失敗");
}
}
| 陷阱 | 後果 | 修正方案 |
|---|---|---|
listFiles() 返回 null | NPE 或跳過刪除 | 權限不足/非目錄/IO 錯誤時返回 null,需判空並記錄錯誤 |
| 符號鏈接循環 | 無限遞歸棧溢出 | 使用 Files.isSymbolicLink() 識別,僅刪除鏈接本身,不跟隨 |
| 文件被其他進程佔用 (Windows) | delete() 返回 false | 重試機制 / 啟動時標記刪除 deleteOnExit() / 使用 Files.deleteIfExists() |
| 路徑過長 (>260 字符 Windows) | 訪問失敗 | 使用 \\\\?\\ 前綴或 NIO.2 Path API |
| 併發修改 (遍歷過程中有新文件創建) | 漏刪/異常 | 快照法:先收集所有路徑到列表,再逆序刪除 |
| 垃圾回收器延遲釋放文件句柄 | 刪除後立即無法創建同名文件 | 顯式關閉流、調用 System.gc() 輔助(不保證) |
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.EnumSet;
public final class SecureFileDeleter {
private SecureFileDeleter() {}
/**
* 安全刪除文件/目錄
* @param path 目標路徑
* @param options 刪除選項
* @return 刪除的文件/目錄總數
* @throws IOException IO 異常
*/
public static long deleteRecursively(Path path, DeleteOption... options) throws IOException {
if (path == null || !Files.exists(path)) {
return 0L;
}
// 配置選項
boolean followLinks = false;
boolean continueOnError = false;
for (DeleteOption opt : options) {
if (opt == DeleteOption.FOLLOW_LINKS) followLinks = true;
if (opt == DeleteOption.CONTINUE_ON_ERROR) continueOnError = true;
}
// 使用 SimpleFileVisitor 遍歷,POST_ORDER 保證先刪子項後刪父目錄
final AtomicLong deletedCount = new AtomicLong();
EnumSet visitOpts = followLinks ? EnumSet.of(FileVisitOption.FOLLOW_LINKS) : EnumSet.noneOf(FileVisitOption.class);
Files.walkFileTree(path, visitOpts, Integer.MAX_VALUE, new SimpleFileVisitor() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
try {
Files.delete(file); // 刪除文件/符號鏈接
deletedCount.incrementAndGet();
} catch (IOException e) {
if (continueOnError) {
System.err.println("刪除文件失敗: " + file + " - " + e.getMessage());
return FileVisitResult.CONTINUE;
}
throw e;
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) {
if (continueOnError) return FileVisitResult.CONTINUE;
throw exc;
}
try {
Files.delete(dir); // 刪除空目錄
deletedCount.incrementAndGet();
} catch (IOException e) {
if (continueOnError) return FileVisitResult.CONTINUE;
throw e;
}
return FileVisitResult.CONTINUE;
}
});
return deletedCount.get();
}
/** 刪除選項枚舉 */
public enum DeleteOption {
FOLLOW_LINKS, // 跟隨符號鏈接刪除目標文件(危險,默認 false)
CONTINUE_ON_ERROR // 單個失敗不中斷,記錄錯誤繼續
}
// 便捷方法
public static long deleteRecursively(String pathStr) throws IOException {
return deleteRecursively(Paths.get(pathStr));
}
public static long deleteRecursively(File file) throws IOException {
return deleteRecursively(file.toPath());
}
}
// 啟用長路徑:需系統策略允許 + Java 10+
Path longPath = Paths.get("\\\\?\\C:\\Very\\Long\\Path\\Exceeding\\260\\Chars.txt");
Files.deleteIfExists(longPath); // NIO.2 自動處理前綴
public static boolean forceDelete(Path path) {
// 1. 嘗試常規刪除
try { return Files.deleteIfExists(path); } catch (IOException ignored) {}
// 2. 標記 JVM 退出時刪除(適用於臨時文件)
path.toFile().deleteOnExit();
// 3. 使用 Windows 原生命令 (最後手段)
if (System.getProperty("os.name").startsWith("Windows")) {
try {
ProcessBuilder pb = new ProcessBuilder("cmd", "/c", "del", "/f", "/q", path.toString());
pb.inheritIO().start().waitFor();
return !Files.exists(path);
} catch (Exception ignored) {}
}
return false;
}
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 點
public void cleanupTempDirs() {
Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"), "myapp");
try {
long deleted = SecureFileDeleter.deleteRecursively(tempDir,
SecureFileDeleter.DeleteOption.CONTINUE_ON_ERROR);
log.info("清理臨時目錄完成: {} 項", deleted);
} catch (IOException e) {
log.error("清理失敗", e);
}
}
| 維度 | java.io.File (Legacy) | java.nio.file (Modern) |
|---|---|---|
| 符號鏈接處理 | 無感知,易陷入循環 | 顯式控制 FOLLOW_LINKS |
| 異常信息豐富度 | 僅返回 boolean | 拋出具體 IOException 子類 |
| 屬性讀取性能 | 每次調用系統調用 | BasicFileAttributes 批量讀取 |
| 流式遍歷 | 無 | Files.walk() 返回 Stream,支持並行 |
| 原子操作 | 無 | Files.move(ATOMIC_MOVE), deleteIfExists |
| 推薦場景 | 簡單腳本、舊版本維護 | 所有新代碼、生產環境 |
@Test
void testDeleteRecursively() throws IOException {
Path root = Files.createTempDir("test-delete");
Files.createDirectories(root.resolve("a/b/c"));
Files.writeString(root.resolve("a/file1.txt"), "1");
Files.writeString(root.resolve("a/b/file2.txt"), "2");
Files.createSymbolicLink(root.resolve("link"), root.resolve("a")); // 符號鏈接
long count = SecureFileDeleter.deleteRecursively(root);
assertEquals(5, count); // a, a/b, a/b/c, file1, file2 (link 不被跟隨刪除)
assertFalse(Files.exists(root));
assertTrue(Files.exists(root.resolve("link"))); // 鏈接本身保留
}
@Test
void testContinueOnError() throws IOException {
Path root = Files.createTempDir("test-error");
Path readonly = root.resolve("readonly");
Files.createFile(readonly);
readonly.toFile().setReadOnly(); // Windows 下可能無效,Linux 有效
long count = SecureFileDeleter.deleteRecursively(root,
SecureFileDeleter.DeleteOption.CONTINUE_ON_ERROR);
// readonly 刪除失敗但繼續執行,不拋出異常
assertTrue(count >= 1);
}
掌握 NIO.2 的 walkFileTree 後序遍歷模式,配合異常隔離與符號鏈接防護,即可構建健壯的文件清理能力。建議在項目公共庫封裝 SecureFileDeleter,統一異常處理、日誌記錄與指標上報。