NIO.2 — Path và Files, API hiện đại
Path thay File, Files utility với 60+ method đồng nhất (readString, write, copy, walk, lines). Watch service theo dõi file realtime. Atomic swap pattern cho ghi file an toàn.
Java 1.0 (1996) có java.io.File — API cho file/directory. Dùng được, nhưng thiết kế có nhiều vấn đề tích luỹ qua 15 năm:
File file = new File("data/report.pdf");
// Check exist
boolean e = file.exists();
// Delete
boolean ok = file.delete(); // Return boolean. Fail -> khong biet ly do.
// Rename
boolean ok2 = file.renameTo(new File("archive/report.pdf"));
// Behavior khac nhau giua Windows va Linux
// Co the fail silent
Vấn đề:
delete()trảboolean. Fail thì sao? Permission? File đang mở? Directory không rỗng? Không biết — không có exception chi tiết.renameTo()hoạt động khác nhau giữa OS. Cross-device rename có thể fail silent.- Không phân biệt file và symlink — operation trên symlink tùy vào JVM version.
- API verbose cho task đơn giản: copy file cần viết loop read/write thủ công.
- Không có utility cho task phổ biến: walk directory recursive, read all lines, compare files.
Java 7 (2011) thêm NIO.2 (java.nio.file): Path, Files, FileSystem. API modern với:
Pathimmutable thayFile— thread-safe, cacheable, platform-aware.Filesstatic utility với 60+ method — copy, move, walk, lines, readString, write.- Throw exception cụ thể với cause — không silent fail.
- Symlink-aware, atomic operation support.
- Watch service cho theo dõi thay đổi file.
Hầu hết code mới nên dùng NIO.2. java.io.File chỉ còn gặp trong codebase cũ hoặc API legacy (vd MultipartFile.transferTo(File) của Spring).
Bài này đi qua Path, method chính của Files, pattern atomic swap, và watch service. 90% task file I/O thực tế cover hết với API này.
1. Path — thay File
Tạo Path
import java.nio.file.*;
Path p1 = Paths.get("data/log.txt"); // Java 7, factory cu
Path p2 = Path.of("data/log.txt"); // Java 11+, recommend
Path p3 = Path.of("/absolute/path/to/file");
Path p4 = Path.of("C:\\Users\\Alice\\doc.pdf"); // Windows
Path p5 = Path.of("data", "subdir", "file.txt"); // Join segments
Path.of (Java 11+) là factory hiện đại — thay cho Paths.get cũ.
Path là immutable
Path p = Path.of("/var/log");
Path child = p.resolve("app.log");
// p van la "/var/log"
// child la "/var/log/app.log" (path moi)
Path abs = p.toAbsolutePath(); // Path moi, p khong doi
Path norm = Path.of("/a/./b/../c").normalize(); // /a/c - path moi
Mỗi op trả path mới — Path không bao giờ mutate. Tương tự String.
Lợi ích:
- Thread-safe: nhiều thread share path không lock.
- Cacheable key: dùng Path làm key trong Map an toàn.
- Method chain:
base.resolve("x").normalize().toAbsolutePath()— rõ intent, không side-effect.
Operation cơ bản
Path p = Path.of("/var/log/app.log");
p.getFileName(); // "app.log" (Path)
p.getParent(); // "/var/log" (Path)
p.getRoot(); // "/" (Path) hoac null neu relative
p.getNameCount(); // 3
p.getName(0); // "var"
p.toString(); // "/var/log/app.log"
p.toFile(); // java.io.File (convert qua lai neu can)
Join và normalize
Path base = Path.of("/home/user");
Path subdir = base.resolve("docs"); // /home/user/docs
Path file = subdir.resolve("report.pdf"); // /home/user/docs/report.pdf
// resolveSibling: thay filename giu parent
Path other = file.resolveSibling("resume.pdf"); // /home/user/docs/resume.pdf
// relativize: tu A ra B
Path rel = base.relativize(file); // docs/report.pdf
// normalize: loai . va ..
Path.of("/a/./b/../c").normalize(); // /a/c
resolve platform-aware — dùng separator đúng (/ Linux, \ Windows). Tránh string concat + "/":
// BAD
Path p = Path.of(base + "/" + filename); // Windows bug
// GOOD
Path p = base.resolve(filename); // Platform correct
2. Files — static utility
Class java.nio.file.Files có 60+ static method cho mọi task file I/O. Thay cho viết loop thủ công.
Check / metadata
Files.exists(path);
Files.notExists(path); // Khac !exists (co the khong determinable)
Files.isRegularFile(path); // La file thuong (khong directory, symlink)
Files.isDirectory(path);
Files.isReadable(path);
Files.isWritable(path);
Files.isExecutable(path);
Files.isHidden(path);
Files.size(path); // bytes
Files.getLastModifiedTime(path); // FileTime
Files.isSameFile(p1, p2); // Chieu sau - kiem tra cung file thuc su (symlink)
Khác biệt exists và !notExists: tồn tại 3 trạng thái — tồn tại, không tồn tại, không biết (vd no permission). notExists true chỉ khi chắc chắn không tồn tại; !exists có thể là "không tồn tại hoặc không xác định".
Create / delete
Files.createDirectories(path); // mkdir -p (tao parent neu can)
Files.createDirectory(path); // throw neu parent khong ton tai
Files.createFile(path); // throw neu ton tai
Files.createTempFile("prefix-", ".tmp"); // tmp file he thong
Files.createTempFile(dir, "prefix-", ".tmp"); // tmp file trong dir
Files.delete(path); // throw neu khong ton tai
Files.deleteIfExists(path); // silent neu khong co
delete throw exception với cause cụ thể:
NoSuchFileException— file không tồn tại.DirectoryNotEmptyException— directory có content.AccessDeniedException— permission.FileSystemException— general OS error.
Khác hẳn File.delete() trả boolean — không biết lý do.
Copy / move
Files.copy(src, dst); // copy
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING); // overwrite
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES); // metadata
Files.move(src, dst); // move/rename
Files.move(src, dst, StandardCopyOption.ATOMIC_MOVE); // atomic
COPY_ATTRIBUTES — giữ timestamp, owner, permission.
ATOMIC_MOVE — rename trong cùng filesystem là atomic (OS rename() syscall). Cross-filesystem không atomic được → throw AtomicMoveNotSupportedException.
3. Đọc file — 3 cách phổ biến
Cách 1: Toàn file vào memory
// Text
String content = Files.readString(path, StandardCharsets.UTF_8); // Java 11+
String contentDefault = Files.readString(path); // Default UTF-8
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
// Binary
byte[] bytes = Files.readAllBytes(path);
Đơn giản, 1 dòng. Nhưng load toàn bộ vào memory — file 1GB → OOM nếu heap < 1GB.
Dùng cho file nhỏ, biết size: config, template, document nhỏ < 100MB.
Cách 2: Stream line-by-line (Java 8+)
try (Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8)) {
long errorCount = lines
.filter(l -> l.contains("ERROR"))
.count();
}
Files.lines trả Stream<String> lazy — đọc từng line khi pipeline yêu cầu. Memory constant (~KB cho buffer + current line).
Bắt buộc try-with-resources — stream giữ file handle, không close → leak FD.
Dùng cho: file text lớn, log, CSV lớn. Chi tiết ở bài 11.4.
Cách 3: BufferedReader (manual loop)
try (BufferedReader r = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = r.readLine()) != null) {
process(line);
}
}
Files.newBufferedReader default UTF-8 (fix bug FileReader platform default). Hỗ trợ readLine như java.io.
Dùng khi: cần control logic loop phức tạp (skip lines, lookahead, conditional parse).
4. Ghi file
Text
// Toan file
Files.writeString(path, "Hello\nWorld\n", StandardCharsets.UTF_8); // Java 11+
Files.write(path, List.of("line1", "line2"), StandardCharsets.UTF_8);
// Append
Files.writeString(path, "more\n", StandardCharsets.UTF_8,
StandardOpenOption.APPEND);
// Atomic: ghi tmp roi rename (pattern chi tiet o muc 6)
Binary
Files.write(path, byteArray);
Streaming write (file lớn)
try (BufferedWriter w = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
for (int i = 0; i < 1_000_000; i++) {
w.write("line " + i + "\n");
}
}
OpenOption
Default Files.write* là: CREATE, TRUNCATE_EXISTING, WRITE. Tức tạo mới hoặc overwrite.
Option khác:
APPEND— ghi thêm vào cuối.CREATE_NEW— throw nếu tồn tại (atomic check-create).DELETE_ON_CLOSE— xoá file khi close.SYNC— fsync sau mỗi write (durability).DSYNC— fsync data only (không metadata).
Files.writeString(path, data, UTF_8,
StandardOpenOption.CREATE_NEW,
StandardOpenOption.SYNC);
// Tao moi, throw neu ton tai, fsync sau write
5. Walking directory — đi qua tree
Files.walk — recursive DFS
try (Stream<Path> paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
Files.walk(root) trả stream tất cả path dưới root theo DFS. Bao gồm root, subdirectory, và file trong đó.
Giới hạn depth:
Files.walk(root, 3); // Toi da depth 3
Files.find — walk + filter trong 1 call
try (Stream<Path> java = Files.find(root, 10,
(p, attrs) -> p.toString().endsWith(".java") && attrs.isRegularFile())) {
java.forEach(System.out::println);
}
BiPredicate nhận (Path, BasicFileAttributes) — filter dùng attribute mà không cần syscall riêng (walk đã lấy attribute rồi).
Nhanh hơn Files.walk().filter(...) cho filter phức tạp.
Files.list — 1 level
try (Stream<Path> children = Files.list(dir)) {
children.forEach(System.out::println);
}
Chỉ trực tiếp con của dir, không recurse. Shell ls.
Quan trọng: mọi stream method của Files giữ file handle. Luôn try-with-resources.
Files.walkFileTree — visitor pattern
Control tinh vi hơn walk — decide visit/skip từng entry:
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
if (dir.getFileName().toString().startsWith(".")) {
return FileVisitResult.SKIP_SUBTREE; // skip hidden directory
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Fail: " + file + " - " + exc);
return FileVisitResult.CONTINUE; // Skip file loi, tiep tuc
}
});
Dùng cho: build index, search engine, backup tool — cần control điều kiện skip/stop.
6. Pattern atomic swap — ghi file an toàn
Use case: service đọc file config định kỳ. Bạn cần update config. Nếu reader đọc đúng lúc writer ghi giữa chừng, reader thấy half-written file → parse lỗi.
Giải pháp: ghi file tmp rồi atomic rename.
Path config = Path.of("/etc/myapp/config.json");
// 1. Ghi tmp trong cung thu muc
Path tmp = Files.createTempFile(config.getParent(), "cfg-", ".tmp");
try {
Files.writeString(tmp, newConfigJson, StandardCharsets.UTF_8);
// 2. Force xuong disk (optional, cho durability)
try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.WRITE)) {
ch.force(true);
}
// 3. Atomic rename
Files.move(tmp, config, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
Files.deleteIfExists(tmp); // Clean up khi fail
throw e;
}
Cơ chế:
- Reader hoặc đọc file cũ toàn vẹn (nếu đọc trước rename), hoặc file mới toàn vẹn (nếu đọc sau). Không bao giờ half-written.
- OS
rename()syscall là atomic trong cùng filesystem.
Điều kiện: tmp và target cùng filesystem. Cross-filesystem rename không atomic — throw AtomicMoveNotSupportedException. Tạo tmp trong parent của target (config.getParent()) đảm bảo cùng filesystem.
Pattern này quan trọng cho: config hot-reload, cache file, database page, snapshot.
7. Watch service — theo dõi thay đổi
Đăng ký watch một thư mục, nhận event khi file trong đó thay đổi:
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("data");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
while (true) {
WatchKey key = watcher.take(); // Block cho event
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path filename = (Path) event.context();
System.out.println(kind + ": " + filename);
}
if (!key.reset()) break; // key invalid -> dung
}
}
Cơ chế bên dưới:
- Linux:
inotifysyscall. - macOS:
FSEventsframework. - Windows:
ReadDirectoryChangesWWin32 API.
Use case:
- Hot reload config: watch
config/→ reload khi file đổi. - File processor: watch incoming directory → process mỗi file mới upload.
- Sync tool: monitor 2 thư mục, sync thay đổi.
Giới hạn:
- Linux
inotifylimit (default 8k watch per user) — watch nhiều folder cần tăngfs.inotify.max_user_watches. - Event có thể lost dưới load cao — không guarantee 100% delivery.
- Sub-directory không auto-watch — đăng ký riêng cho từng sub-directory.
Cho production reliable, consider library như Apache Commons IO FileAlterationMonitor — wrap watch service với polling backup.
8. File attribute
Ngoài basic (size, mtime), NIO.2 hỗ trợ attribute theo OS:
// Basic - platform independent
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
attrs.creationTime();
attrs.lastModifiedTime();
attrs.size();
attrs.isRegularFile();
// POSIX (Linux, macOS)
PosixFileAttributes posix = Files.readAttributes(path, PosixFileAttributes.class);
posix.owner(); // UserPrincipal
posix.group();
Set<PosixFilePermission> perms = posix.permissions();
PosixFilePermissions.toString(perms); // "rwxr-x---"
// Set permission
Files.setPosixFilePermissions(path,
PosixFilePermissions.fromString("rw-r--r--"));
// DOS (Windows)
DosFileAttributes dos = Files.readAttributes(path, DosFileAttributes.class);
dos.isReadOnly();
dos.isHidden();
dos.isArchive();
Platform-specific attribute throw UnsupportedOperationException nếu gọi trên filesystem không hỗ trợ (vd POSIX trên NTFS).
9. So sánh java.io.File và NIO.2
| Task | java.io.File | NIO.2 |
|---|---|---|
| Tạo path | new File("x.txt") | Path.of("x.txt") |
| Đọc text toàn file | Tự build stream chain | Files.readString(p) |
| Đọc text stream | BufferedReader chain | Files.lines(p) + Stream API |
| Copy | Tự loop read/write | Files.copy(src, dst, options...) |
| Move | renameTo() fragile | Files.move(src, dst, ATOMIC_MOVE) |
| Walk tree | listFiles() đệ quy thủ công | Files.walk(root) |
| Delete fail | boolean không biết lý do | Throw exception cụ thể |
| Charset | Default platform (bug Windows) | Explicit trong API |
| Symlink | Không phân biệt | Files.readSymbolicLink, NOFOLLOW_LINKS option |
| Immutable | File mutable field | Path immutable |
| Watch changes | Không có | WatchService |
Quy tắc: code mới → NIO.2. java.io.File chỉ còn gặp khi interop với legacy API (Spring MultipartFile, ImageIO, etc.) — convert qua Path.toFile() / File.toPath().
10. Pitfall tổng hợp
❌ Nhầm 1: Files.lines không close.
Files.lines(path).filter(...).count(); // Leak FD
✅ Try-with-resources:
try (Stream<String> lines = Files.lines(path)) {
lines.filter(...).count();
}
❌ Nhầm 2: Files.readAllBytes cho file lớn.
byte[] data = Files.readAllBytes(path); // OOM neu 10GB
✅ Stream-based: Files.newInputStream(path) hoặc Files.lines(path).
❌ Nhầm 3: Files.walk không close.
Files.walk(root).forEach(...); // Stream giu file handle
✅ Try-with-resources.
❌ Nhầm 4: Path concat bằng +.
Path p = Path.of(base + "/" + filename); // Bug separator Windows
✅ base.resolve(filename).
❌ Nhầm 5: Files.copy không option → throw khi đích tồn tại.
Files.copy(src, dst); // FileAlreadyExistsException
✅ Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING) nếu muốn overwrite.
❌ Nhầm 6: Atomic move cross-filesystem.
Files.move(/tmp/x, /home/user/x, ATOMIC_MOVE); // AtomicMoveNotSupportedException neu khac FS
✅ Tạo tmp trong cùng parent của target:
Path tmp = Files.createTempFile(target.getParent(), "tmp-", ".tmp");
❌ Nhầm 7: Dùng File.delete() không check return.
file.delete(); // Return boolean - fail thi sao?
✅ NIO.2 throw exception cụ thể:
Files.delete(path); // Throw NoSuchFileException / AccessDeniedException / ...
11. 📚 Deep Dive Oracle
Spec / reference chính thức:
- java.nio.file package — overview.
- Path interface — tất cả method.
- Files class — 60+ static utility method.
- JSR 203: NIO.2 — spec đầu tiên của NIO.2, Java 7.
- Tutorial: File I/O (NIO.2) — Oracle tutorial comprehensive.
- StandardCopyOption, StandardOpenOption — option enum.
Ghi chú: Javadoc Files class mô tả từng op đi kèm IOException cause cụ thể — đọc section "File Systems" để hiểu vì sao Files.move cross-filesystem có thể fail. Tutorial Oracle có chương riêng "Walking the File Tree" với pattern visitor detailed — essential khi viết tool backup / search.
12. Tóm tắt
Path(NIO.2) thayFile(java.io): immutable, platform-aware, thread-safe.Path.of(Java 11+) là factory hiện đại thay choPaths.get.Files.readString/writeStringcho text nhỏ;Files.readAllBytes/writecho binary nhỏ.Files.linesstream lazy — phải close try-with-resources để không leak FD.Files.walk(root)recursive;Files.list(dir)1 level;Files.find(root, depth, predicate)walk+filter.Files.walkFileTree(root, visitor)— visitor pattern với SKIP_SUBTREE, TERMINATE cho control chi tiết.Files.copy/movevớiStandardCopyOption:REPLACE_EXISTING,ATOMIC_MOVE,COPY_ATTRIBUTES.Files.newBufferedReader/Writer(path)— default UTF-8, không platform default.- Atomic swap pattern: tạo tmp trong cùng parent → ghi →
Files.move(ATOMIC_MOVE). Đảm bảo reader không thấy half-written. WatchServicecho theo dõi file thay đổi realtime (inotify Linux, FSEvents macOS).- Exception NIO.2 cụ thể (
NoSuchFileException,AccessDeniedException) — khácFiletrả boolean. java.io.Filelegacy — dùng NIO.2 cho code mới, convert qua lại bằngtoPath()/toFile()khi cần interop.
13. Tự kiểm tra
Q1Vì sao Files.lines(path).count() có thể leak file descriptor?▸
Files.lines(path).count() có thể leak file descriptor?Files.lines mở file và trả Stream<String> giữ file handle bên dưới. Intermediate op (filter, map) không đóng. Terminal op (count) duyệt stream xong cũng không đóng — file descriptor giữ đến khi GC dọn object stream.
GC timing không xác định — có thể phút, giờ, hoặc khi heap pressure. Trong lúc đó, file handle chiếm 1 slot FD của process. Linux default limit 1024 FD, macOS thấp hơn.
Stream implement AutoCloseable chính vì lý do này. Phải try-with-resources:
try (Stream<String> lines = Files.lines(path)) {
long count = lines.count();
}Hậu quả leak: service 24/7 chạy lâu → "Too many open files" error → không mở được file/socket mới → crash. Hiếm gặp trong dev (chạy ngắn), phổ biến trong production.
Q2Khi nào dùng Files.readAllBytes vs Files.lines?▸
- readAllBytes / readAllLines / readString: file nhỏ, biết size (< 100MB tuỳ heap). Load toàn bộ vào memory, API đơn giản. Dùng cho config, template, document ngắn.
- Files.lines / newBufferedReader / newInputStream: file lớn hoặc không biết size. Stream lazy, memory constant (~KB). Dùng cho log, CSV lớn, data file.
Rule thực tế:
- Biết file nhỏ (hardcoded asset, config kiểm soát) →
readStringtiện. - File từ user upload, từ API không kiểm soát size → stream-based để tránh DoS qua file lớn.
- Log, audit, data pipeline → stream-based mặc định.
Anti-pattern: Files.readAllBytes trên file log 10GB → OOM ngay.
Q3Khác biệt giữa Files.copy(src, dst) không option và có REPLACE_EXISTING?▸
Files.copy(src, dst) không option và có REPLACE_EXISTING?- Không option: nếu
dsttồn tại → throwFileAlreadyExistsException. Safe default — không overwrite ngẫu nhiên. - REPLACE_EXISTING: overwrite
dst. Cần khi biết chắc muốn replace (vd cập nhật cache, sync file).
Default "fail khi conflict" là design đúng của NIO.2 — khác với Unix cp command line mặc định overwrite không hỏi. Buộc dev suy nghĩ rõ intent.
Nếu dùng File.copy legacy (hay `FileUtils` của Commons IO) có semantic khác nhau giữa library — dễ bug. NIO.2 nhất quán: option explicit.
Thêm option COPY_ATTRIBUTES nếu cần giữ timestamp, owner, permission. Không default vì performance — copy attribute tốn syscall thêm.
Q4Vì sao Path.of immutable là thiết kế đúng?▸
3 lý do kỹ thuật:
- Thread safety: nhiều thread share cùng Path object không cần lock. Không có race condition vì không ai mutate được.
- Cacheable key: dùng Path làm key trong
HashMap,ConcurrentHashMapan toàn — hash code không đổi.Filemutable sẽ bug nếu modify khi đang làm key. - Method chaining rõ:
base.resolve("x").normalize().toAbsolutePath()— mỗi bước trả path mới, không side-effect. Reader hiểu ngay.
Tương tự String, Instant, LocalDate — API modern Java ưu tiên immutable.
Trade-off: mỗi op tạo object mới → tốn allocation. Với Path ngắn (< 10 segment), overhead không đáng kể. GC modern (G1, ZGC) xử lý short-lived object hiệu quả.
Nếu mutable: path.setName("x") sẽ ảnh hưởng code khác giữ reference cùng path — bug khó debug, không type-safe.
Q5Pattern atomic swap cho ghi file hoạt động thế nào, và khi nào cần dùng?▸
Pattern 3 bước:
- Tạo tmp file trong cùng thư mục của target:
Files.createTempFile(target.getParent(), "tmp-", ".tmp"). Cùng parent đảm bảo cùng filesystem —ATOMIC_MOVEchỉ đảm bảo trong 1 filesystem. - Ghi data đầy đủ vào tmp, flush/fsync nếu cần durability.
- Atomic rename tmp sang target:
Files.move(tmp, target, ATOMIC_MOVE, REPLACE_EXISTING). OSrename()syscall là atomic trong cùng filesystem.
Kết quả: reader bất cứ lúc nào cũng thấy hoặc file cũ toàn vẹn, hoặc file mới toàn vẹn. Không bao giờ thấy half-written.
Khi nào dùng:
- Config hot-reload: service đọc config định kỳ. Update config không làm service parse lỗi.
- Cache file: update cache atomic, reader không thấy inconsistent state.
- Database page / WAL: essential cho durability.
- Snapshot / checkpoint: app crash giữa save không corrupt snapshot cũ.
Cross-filesystem rename → AtomicMoveNotSupportedException. Phải tạo tmp trong cùng filesystem với target. Dùng Files.createTempFile(parent, ...) không dùng Files.createTempFile(prefix, suffix) (tạo trong /tmp có thể khác filesystem).
Q6Khi nào dùng Files.walkFileTree thay Files.walk?▸
Files.walkFileTree thay Files.walk?- Files.walk: stream đơn giản, dùng stream API filter/map. Phù hợp task nhanh "tìm file theo pattern".
- Files.walkFileTree + visitor: control chi tiết — decide visit/skip/terminate từng entry. Phù hợp tool phức tạp.
Khi nào cần visitor:
- Skip sub-directory theo điều kiện: skip
.git,node_modules— không cần duyệt content.preVisitDirectory(dir, attrs) { if (dir.getFileName().toString().equals(".git")) { return SKIP_SUBTREE; } return CONTINUE; } - Handle IO error per-file: file no permission → log warning, continue thay crash cả walk.
visitFileFailed(file, exc) { log.warn("Skip " + file, exc); return CONTINUE; } - Terminate sớm: tìm thấy result, không cần duyệt tiếp.
return TERMINATE; - Post-order processing: xử lý sau khi visit hết children (vd tính size thư mục).
Tool backup, search engine, antivirus — dùng walkFileTree. Script đơn giản — Files.walk đủ.
Bài tiếp theo: Serialization — Serializable và những cạm bẫy
Bài này có giúp bạn hiểu bản chất không?