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:
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, 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
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. 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.
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 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+)
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)
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
// 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
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. 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) |
| 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 |
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.
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: Path concat bằng +.
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.
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.
file.delete(); // Return boolean - fail thi sao?
✅ NIO.2 throw exception cụ thể:
Files.delete(path); // Throw NoSuchFileException / AccessDeniedException / ...
7. 📚 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. 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) thayFile(java.io): immutable, platform-aware, thread-safe.Path.of(Java 11+) là factory hiện đại thay choPaths.get.- Nối path bằng
resolve— không string concat (bug separator Windows). Files.readString/writeStringcho text nhỏ;Files.readAllBytes/writecho binary nhỏ.Files.linesstream lazy cho file lớn — phải close try-with-resources để không leak FD.Files.copy/movevớiStandardCopyOption: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ácFiletrả boolean. java.io.Filelegacy — dùng NIO.2 cho code mới, convert qua lại bằngtoPath()/toFile()khi cần interop.
9. 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 API copy legacy (hay FileUtils của Commons IO) thì 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.
Q5Vì sao NIO.2 chọn throw exception cụ thể thay vì trả boolean như File.delete()?▸
boolean như File.delete()?File.delete() trả false gộp chung mọi nguyên nhân fail: file không tồn tại, không có permission, directory chưa rỗng, file đang bị process khác giữ. Caller không phân biệt được → hoặc bỏ qua silent (bug âm thầm), hoặc đoán mò để retry.
NIO.2 tách từng nguyên nhân thành exception riêng: NoSuchFileException, AccessDeniedException, DirectoryNotEmptyException, FileSystemException. Caller xử lý đúng từng case:
- Không tồn tại → có thể coi là thành công (idempotent delete) — hoặc dùng thẳng
deleteIfExists. - Permission → log + báo lỗi cấu hình, không retry vô ích.
- Directory chưa rỗng → xoá content trước rồi retry.
Đây cũng là lý do checked IOException trong chữ ký method: I/O fail là chuyện bình thường (disk đầy, file bị xoá bởi process khác) — API buộc caller đối diện với nó thay vì giả vờ mọi thứ luôn thành công.
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?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên