Java — Từ Zero đến Senior/NIO.2 — Path và Files, API hiện đại
~22 phútI/O & NIOMiễn phí

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:

  • Path immutable thay File — thread-safe, cacheable, platform-aware.
  • Files static 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ớiPath 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!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: inotify syscall.
  • macOS: FSEvents framework.
  • Windows: ReadDirectoryChangesW Win32 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 inotify limit (default 8k watch per user) — watch nhiều folder cần tăng fs.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

Taskjava.io.FileNIO.2
Tạo pathnew File("x.txt")Path.of("x.txt")
Đọc text toàn fileTự build stream chainFiles.readString(p)
Đọc text streamBufferedReader chainFiles.lines(p) + Stream API
CopyTự loop read/writeFiles.copy(src, dst, options...)
MoverenameTo() fragileFiles.move(src, dst, ATOMIC_MOVE)
Walk treelistFiles() đệ quy thủ côngFiles.walk(root)
Delete failboolean không biết lý doThrow exception cụ thể
CharsetDefault platform (bug Windows)Explicit trong API
SymlinkKhông phân biệtFiles.readSymbolicLink, NOFOLLOW_LINKS option
ImmutableFile mutable fieldPath immutable
Watch changesKhô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

📚 Deep Dive Oracle

Spec / reference chính thức:

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) thay File (java.io): immutable, platform-aware, thread-safe.
  • Path.of (Java 11+) là factory hiện đại thay cho Paths.get.
  • Files.readString/writeString cho text nhỏ; Files.readAllBytes/write cho binary nhỏ.
  • Files.lines stream 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/move với StandardCopyOption: 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.
  • WatchService cho theo dõi file thay đổi realtime (inotify Linux, FSEvents macOS).
  • Exception NIO.2 cụ thể (NoSuchFileException, AccessDeniedException) — khác File trả boolean.
  • java.io.File legacy — dùng NIO.2 cho code mới, convert qua lại bằng toPath() / toFile() khi cần interop.

13. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao 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.

Q2
Khi 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) → readString tiệ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.

Q3
Khác biệt giữa Files.copy(src, dst) không option và có REPLACE_EXISTING?
  • Không option: nếu dst tồn tại → throw FileAlreadyExistsException. 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.

Q4
Vì sao Path.of immutable là thiết kế đúng?

3 lý do kỹ thuật:

  1. 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.
  2. Cacheable key: dùng Path làm key trong HashMap, ConcurrentHashMap an toàn — hash code không đổi. File mutable sẽ bug nếu modify khi đang làm key.
  3. 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.

Q5
Pattern atomic swap cho ghi file hoạt động thế nào, và khi nào cần dùng?

Pattern 3 bước:

  1. 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_MOVE chỉ đảm bảo trong 1 filesystem.
  2. Ghi data đầy đủ vào tmp, flush/fsync nếu cần durability.
  3. Atomic rename tmp sang target: Files.move(tmp, target, ATOMIC_MOVE, REPLACE_EXISTING). OS rename() 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).

Q6
Khi nào dùng 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?