文件操作是 Java 開發的基礎功,但 java.io.File (Legacy IO) 與 java.nio.file (NIO.2) 並存導致 API 混亂、異常處理繁瑣、編碼問題頻發。本文系統梳理 **NIO.2 為主、Legacy 為輔** 的現代最佳實踐,涵蓋建立/刪除/複製/移動/遍歷/讀寫/屬性/監聽全生命週期,並給出生產級工具類與避坑清單。
| 場景 | 推薦 API (NIO.2) | Legacy IO (棄用/遷移中) |
|---|---|---|
| 路徑表示 | Path, Paths.get() | File |
| 建立/刪除/複製/移動 | Files.createFile/Dir/delete/copy/move | File.createNewFile/mkdir/delete/renameTo |
| 遍歷目錄樹 | Files.walk/walkFileTree | File.listFiles() 遞歸 |
| 讀取文本 (小文件) | Files.readAllLines/readString (Java 11+) | BufferedReader + FileReader |
| 讀取文本 (大文件/流式) | Files.lines() → Stream | BufferedReader |
| 寫入文本 | Files.write/writeString | FileWriter/BufferedWriter |
| 二進制讀寫 | Files.readAllBytes/newInputStream/newOutputStream | FileInputStream/OutputStream |
| 屬性讀取 | Files.readAttributes/BasicFileAttributes | File.length/lastModified/isDirectory |
| 文件監聽 | WatchService | 無 |
Path path = Paths.get("/data", "logs", "app.log").normalize().toAbsolutePath();
// 正規化:解析 . .. 符號鏈接 → 絕對路徑
// 跨平台分隔符自動處理,Windows 也可用正斜線
// 建立多級目錄,存在不報錯
Files.createDirectories(Paths.get("/data/logs/2024/01"));
// 建立文件,可設置權限 (POSIX)
Files.createFile(path, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-r--r--")));
// 檢查存在性
if (Files.notExists(path)) { ... }
// 或原子建立:CREATE_NEW 失敗拋 FileAlreadyExistsException
Files.createFile(path, StandardOpenOption.CREATE_NEW);
// 刪除文件/空目錄,不存在返回 false 不拋異常
Files.deleteIfExists(path);
// 遞歸刪除非空目錄 (Java 8+)
Files.walkFileTree(dir, new SimpleFileVisitor() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file); return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir); return FileVisitResult.CONTINUE;
}
});
// Java 11+ 一行刪除目錄樹 (內部實現同上)
Files.deleteIfExists(dir); // 僅空目錄
// 非空目錄仍需 walkFileTree 或引入 Apache Commons IO: FileUtils.deleteDirectory(dir.toFile())
Path src = Paths.get("/data/source.txt");
Path dst = Paths.get("/data/backup/target.txt");
// 複製:覆蓋、複製屬性、原子移動
Files.copy(src, dst,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES,
LinkOption.NOFOLLOW_LINKS);
// 移動:同檔案系統原子重命名,跨檔案系統複製+刪除
Files.move(src, dst,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE); // 同卷原子,跨卷忽略
// 1. 深度優先流式遍歷 (內存友好,可中斷)
try (Stream stream = Files.walk(root, 3, FileVisitOption.FOLLOW_LINKS)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
}
// 2. 訪問者模式 (完全控制、可修改樹)
Files.walkFileTree(root, new SimpleFileVisitor() {
@Override public FileVisitResult visitFileFailed(Path file, IOException exc) {
log.warn("無法訪問:{}", file, exc); return FileVisitResult.CONTINUE; // 跳過錯誤繼續
}
@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().startsWith(".")) return FileVisitResult.SKIP_SUBTREE; // 跳過隱藏目錄
return FileVisitResult.CONTINUE;
}
});
// 3. 只列一層 (ls 效果)
try (DirectoryStream stream = Files.newDirectoryStream(dir, "*.log")) {
for (Path entry : stream) { ... }
}
// 小文件一次性讀取 (Java 11+)
List lines = Files.readAllLines(path, StandardCharsets.UTF_8);
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+
// 大文件流式處理 (自動關閉 Stream)
try (Stream lines = Files.lines(path, StandardCharsets.UTF_8)) {
long errorCount = lines.filter(l -> l.contains("ERROR")).count();
}
// 寫入:原子寫入 (寫臨時文件 + 原子 move)
Path tmp = Files.createTempFile(path.getParent(), "tmp", ".log");
Files.write(tmp, lines, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
// 追加寫入
Files.write(path, List.of("new line"), StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// 讀取全部位元組 (小文件)
byte[] data = Files.readAllBytes(path);
// 流式複製 (大文件,緩衝區 8KB)
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) out.write(buffer, 0, len);
}
// 內存映射 (極大文件、隨機讀寫、高效能)
try (FileChannel ch = FileChannel.open(path, StandardOpenOption.READ)) {
MappedByteBuffer mbb = ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size());
// 直接操作 mbb,OS 負責頁面調度
}
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.size() // 大小
attrs.creationTime() // 建立時間
attrs.lastModifiedTime() // 修改時間
attrs.isDirectory() // 是否目錄
attrs.isRegularFile() // 是否普通文件
attrs.isSymbolicLink() // 是否符號鏈接
// POSIX 權限 (Linux/macOS)
Set perms = Files.getPosixFilePermissions(path);
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxr-x---"));
// Windows ACL (Java 17+)
AclFileAttributeView aclView = Files.getFileAttributeView(path, AclFileAttributeView.class);
List acl = aclView.getAcl();
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/data/watch");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
WatchKey key = watcher.take(); // 阻塞
for (WatchEvent> event : key.pollEvents()) {
Path changed = dir.resolve((Path) event.context());
System.out.println(event.kind() + " -> " + changed);
}
boolean valid = key.reset();
if (!valid) break; // 目錄不可訪問
}
public final class FileUtils {
private FileUtils() {}
/** 原子寫入文本:臨時文件 + 原子移動,防止寫入中斷產生髒文件 */
public static void writeStringAtomically(Path path, String content, Charset charset) throws IOException {
Path tmp = Files.createTempFile(path.getParent(), "atomic-", ".tmp");
try {
Files.writeString(tmp, content, charset, StandardOpenOption.CREATE_NEW);
Files.move(tmp, path, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (IOException e) {
Files.deleteIfExists(tmp); // 清理臨時文件
throw e;
}
}
/** 複製目錄樹:保留屬性、符號鏈接選項、進度回調 */
public static long copyDirectory(Path src, Path dst, CopyOption... options) throws IOException {
AtomicLong copied = new AtomicLong();
Files.walkFileTree(src, new SimpleFileVisitor() {
@Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path target = dst.resolve(src.relativize(dir));
Files.createDirectories(target, Arrays.stream(options).filter(o -> o instanceof LinkOption).toArray(CopyOption[]::new));
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path target = dst.resolve(src.relativize(file));
Files.copy(file, target, options);
copied.addAndGet(attrs.size());
return FileVisitResult.CONTINUE;
}
});
return copied.get();
}
/** 計算目錄大小 (位元組) */
public static long sizeOf(Path path) throws IOException {
if (Files.isRegularFile(path)) return Files.size(path);
return Files.walk(path).filter(Files::isRegularFile).mapToLong(p -> {
try { return Files.size(p); } catch (IOException e) { return 0L; }
}).sum();
}
}
StandardCharsets.UTF_8。File.delete() 失敗不拋異常 → 靜默失敗:改用 Files.deleteIfExists() 判斷返回值,或 Files.delete() 捕獲 NoSuchFileException/DirectoryNotEmptyException。renameTo() 跨檔案系統失敗、非原子:改用 Files.move(..., ATOMIC_MOVE)。readAllBytes/readAllLines → OOM:超過 100MB 必用流式 Files.lines() / InputStream / MappedByteBuffer。walkFileTree 覆寫 visitFileFailed 返回 CONTINUE,preVisitDirectory 檢測循環返回 SKIP_SUBTREE。Files.deleteIfExists 返回 false,需重試或排查佔用進程 (Handle/Process Explorer)。path.toAbsolutePath().normalize() 落盤/傳遞。Path, Files, FileChannel) 為主力,Legacy IO (File, FileInputStream) 僅維護舊代碼。