Java Internals & Concurrency
40/75
Bài 40 / 75~13 phútI/O & NIOMiễn phí… lượt xem

Path và Files — NIO.2 thay java.io.File

Path immutable thay File, Files utility đồng nhất: readString, write, copy, move, delete với exception cụ thể. Vì sao API 1996 phải thay, và 90% task file I/O hằng ngày gói trong vài dòng.

TL;DR: java.io.File (1996) trả boolean khi fail, hành xử khác nhau giữa OS, và bắt bạn tự viết loop cho task phổ biến. Java 7 thêm NIO.2 (java.nio.file): Path immutable + platform-aware thay File; Files với 60+ static method (readString, writeString, copy, move, delete) throw exception cụ thể thay vì fail silent. Quy tắc: code mới luôn dùng NIO.2; đọc file nhỏ dùng readString/readAllBytes, file lớn dùng Files.lines lazy (nhớ try-with-resources); nối path bằng resolve, không string concat. java.io.File chỉ còn gặp khi interop với API legacy.

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:

Java
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, lines, readString, write.
  • Throw exception cụ thể với cause — không silent fail.
  • Symlink-aware, atomic operation support.

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 và các method nền tảng của Files: check/metadata, create/delete, copy/move, đọc và ghi file. Phần duyệt cây thư mục, atomic swap và WatchService để dành cho bài kế tiếp — Duyệt thư mục và theo dõi file.

1. Path — thay File

Tạo Path

Java
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

Java
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

Java
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

Java
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 + "/":

Java
// 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

Java
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

Java
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

Java
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. Pattern "ghi tmp rồi atomic rename" để ghi file an toàn được mổ chi tiết ở bài Duyệt thư mục và theo dõi file.

Thử ngẫmcontainer của bạn mount /tmp và /data trên hai volume khác nhau. Pattern ghi-tmp-rồi-ATOMIC_MOVE còn an toàn ở đây không?

3. Đọc file — 3 cách phổ biến

Cách 1: Toàn file vào memory

Java
// 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 dưới 1GB.

Dùng cho file nhỏ, biết size: config, template, document nhỏ dưới 100MB.

Cách 2: Stream line-by-line (Java 8+)

Java
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 Stream file — Files.lines.

Cách 3: BufferedReader (manual loop)

Java
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 ở bài I/O cổ điển). 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

Java
// 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);

Binary

Java
Files.write(path, byteArray);

Streaming write (file lớn)

Java
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).
Java
Files.writeString(path, data, UTF_8,
    StandardOpenOption.CREATE_NEW,
    StandardOpenOption.SYNC);
// Tao moi, throw neu ton tai, fsync sau write

Thử ngẫmbạn thêm StandardOpenOption.SYNC vào mọi lần ghi log để tránh mất dữ liệu khi crash. Cái giá đánh đổi trực tiếp ở mỗi lần ghi là gì?

5. 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)
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

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().

6. Pitfall tổng hợp

❌ Nhầm 1: Files.lines không close.

Java
Files.lines(path).filter(...).count();   // Leak FD

✅ Try-with-resources:

Java
try (Stream<String> lines = Files.lines(path)) {
    lines.filter(...).count();
}

❌ Nhầm 2: Files.readAllBytes cho file lớn.

Java
byte[] data = Files.readAllBytes(path);   // OOM neu 10GB

✅ Stream-based: Files.newInputStream(path) hoặc Files.lines(path).

❌ Nhầm 3: Path concat bằng +.

Java
Path p = Path.of(base + "/" + filename);   // Bug separator Windows

✅ base.resolve(filename).

❌ Nhầm 4: Files.copy không option → throw khi đích tồn tại.

Java
Files.copy(src, dst);   // FileAlreadyExistsException

✅ Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING) nếu muốn overwrite.

❌ Nhầm 5: Dùng File.delete() không check return.

Java
file.delete();   // Return boolean - fail thi sao?

✅ NIO.2 throw exception cụ thể:

Java
Files.delete(path);   // Throw NoSuchFileException / AccessDeniedException / ...

7. 📚 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. Spec NIO.2 (JSR 203) cũng giải thích design rationale: vì sao tách Path (định danh) khỏi Files (operation).

8. 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.
  • Nối path bằng resolve — không string concat (bug separator Windows).
  • Files.readString/writeString cho text nhỏ; Files.readAllBytes/write cho binary nhỏ.
  • Files.lines stream lazy cho file lớn — phải close try-with-resources để không leak FD.
  • Files.copy/move với StandardCopyOption: REPLACE_EXISTING, ATOMIC_MOVE, COPY_ATTRIBUTES.
  • Files.newBufferedReader/Writer(path) — default UTF-8, không platform default.
  • 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.

9. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao Files.lines(path).count() có thể leak file descriptor?
  2. Q2
    Khi nào dùng Files.readAllBytes vs Files.lines?
  3. Q3
    Khác biệt giữa Files.copy(src, dst) không option và có REPLACE_EXISTING?
  4. Q4
    Vì sao Path.of immutable là thiết kế đúng?
  5. Q5
    Vì sao NIO.2 chọn throw exception cụ thể thay vì trả boolean như File.delete()?

Bài tiếp theo: Duyệt thư mục và theo dõi file — walk, atomic swap, WatchService

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Duyệt thư mục và theo dõi file — walk, atomic swap, WatchService