1. Ghi đè (overwrite)
Khi bạn làm việc với tệp, điều quan trọng là hiểu rằng “ghi vào tệp” không phải lúc nào cũng giống nhau. Đôi khi cần ghi đè hoàn toàn tệp (ví dụ, khi bạn lưu một báo cáo mới), còn đôi khi — thêm thông tin mới vào cuối tệp đã tồn tại (ví dụ, ghi log sự kiện của ứng dụng). Trong một số trường hợp, bạn chỉ cần đọc nội dung tệp mà không thay đổi nó.
Trong Java (đặc biệt trong gói hiện đại java.nio.file), chế độ làm việc với tệp được xác định bởi một tập tùy chọn đặc biệt mà bạn truyền vào các phương thức ghi, ví dụ Files.write(). Những tùy chọn này cho phép bạn chỉ rõ rõ ràng là bạn muốn ghi đè tệp hay thêm dữ liệu vào cuối.
Cách hoạt động
Khi bạn gọi Files.write(path, data), theo mặc định Java sẽ tạo tệp mới hoặc ghi đè tệp đã tồn tại. Toàn bộ nội dung cũ của tệp sẽ bị xóa và chỉ còn dữ liệu mới.
Hành vi mặc định này — một dạng “reset cứng” cho tệp. Nếu trong tệp trước đó có 1000 dòng, mà bạn ghi một dòng — mọi thứ trước đó sẽ biến mất hoàn toàn.
Ví dụ: ghi chuỗi vào tệp
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class OverwriteFileExample {
public static void main(String[] args) throws IOException {
Path path = Paths.get("myfile.txt");
List<String> lines = List.of("Xin chào, thế giới!", "Đây là bản ghi mới trong tệp.");
// Ghi các dòng vào tệp (nội dung cũ sẽ bị xóa)
Files.write(path, lines);
System.out.println("Tệp đã được ghi đè thành công!");
}
}
Sau khi chạy đoạn mã này, trong tệp myfile.txt sẽ chỉ còn hai dòng từ danh sách lines. Mọi thứ trước đó sẽ biến mất (không có bất kỳ “tiếc nuối” nào từ phía Java).
2. Bổ sung (append): thêm dữ liệu vào cuối tệp
Trong một số tác vụ (ví dụ, ghi nhật ký sự kiện, logging, tích lũy dữ liệu) bạn không muốn xóa nội dung cũ của tệp, mà thêm các dòng mới vào cuối. Trong API cũ người ta thường dùng FileWriter với cờ append = true. Trong API hiện đại, mọi thứ được làm đơn giản và rõ ràng hơn.
Cách thực hiện
Bạn chỉ cần truyền tùy chọn StandardOpenOption.APPEND vào phương thức Files.write():
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
public class AppendFileExample {
public static void main(String[] args) throws IOException {
Path path = Paths.get("myfile.txt");
List<String> moreLines = List.of("Đã thêm một dòng nữa.", "Và thêm một dòng nữa!");
// Thêm các dòng vào cuối tệp (nội dung cũ được giữ lại)
Files.write(path, moreLines, StandardOpenOption.APPEND);
System.out.println("Đã thêm các dòng vào cuối tệp!");
}
}
Điểm quan trọng: nếu tệp không tồn tại, khi cố gắng bổ sung sẽ phát sinh lỗi — Java sẽ không tự động tạo tệp ở chế độ APPEND. Để tạo tệp nếu chưa có, hãy dùng đồng thời hai tùy chọn:
Files.write(path, moreLines, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
Trông như thế nào trong thực tế
- Bạn chạy chương trình với chế độ ghi (ghi đè) — trong tệp có hai dòng.
- Chạy chương trình với chế độ bổ sung — các dòng mới sẽ được thêm vào, các dòng cũ vẫn giữ nguyên.
3. Kết hợp tùy chọn: CREATE, APPEND, TRUNCATE_EXISTING
Trong Java, bạn có thể kết hợp nhiều tùy chọn để điều khiển linh hoạt hơn chế độ mở tệp:
- StandardOpenOption.CREATE — tạo tệp nếu chưa có.
- StandardOpenOption.APPEND — thêm dữ liệu vào cuối.
- StandardOpenOption.TRUNCATE_EXISTING — cắt tệp về 0 byte (xóa sạch) nếu nó tồn tại.
- StandardOpenOption.CREATE_NEW — tạo tệp mới; nếu đã tồn tại — báo lỗi.
Ví dụ: tạo tệp nếu chưa có và thêm dữ liệu vào cuối
Files.write(path, moreLines, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
Ví dụ: tạo tệp hoặc xóa sạch và ghi dữ liệu mới
Files.write(path, lines, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
Bảng: các chế độ ghi chính
| Tùy chọn | Hành vi |
|---|---|
| (mặc định) | Tạo tệp hoặc ghi đè tệp hiện có |
|
Thêm vào cuối, lỗi nếu không có tệp |
|
Thêm vào cuối, tạo tệp nếu chưa có |
|
Cắt tệp về 0 và ghi dữ liệu mới |
|
Tạo tệp mới, lỗi nếu tệp đã tồn tại |
4. Đọc và ghi tệp nhị phân
Đến giờ chúng ta làm việc với chuỗi. Nhưng đôi khi cần ghi hoặc đọc các byte “thô” (ví dụ, hình ảnh, tệp nén, tệp PDF). Khi đó hãy dùng:
- Files.readAllBytes(path) — đọc toàn bộ tệp vào mảng byte.
- Files.write(path, byteArray) — ghi mảng byte vào tệp.
Ví dụ: sao chép tệp
import java.nio.file.*;
import java.io.IOException;
public class CopyBinaryFileExample {
public static void main(String[] args) throws IOException {
Path source = Paths.get("logo.png");
Path target = Paths.get("logo_copy.png");
byte[] data = Files.readAllBytes(source);
Files.write(target, data);
System.out.println("Tệp đã được sao chép!");
}
}
Ví dụ: bổ sung dữ liệu nhị phân
byte[] moreData = new byte[] {1, 2, 3, 4, 5};
Files.write(path, moreData, StandardOpenOption.APPEND, StandardOpenOption.CREATE);
Lưu ý: bổ sung dữ liệu nhị phân vào một tệp vốn đã chứa dữ liệu có cấu trúc (ví dụ, ảnh) thường sẽ làm hỏng tệp. Chỉ dùng bổ sung cho tệp văn bản hoặc các tệp nhị phân đã được chuẩn bị đặc biệt (ví dụ, log).
5. Khi nào cần stream (FileInputStream, BufferedReader và các loại khác)
Các phương thức Files.readAllBytes(), Files.write() tiện cho tệp nhỏ và vừa, khi có thể nạp toàn bộ nội dung vào bộ nhớ một lần. Nếu tệp lớn (ví dụ, hàng gigabyte), hoặc bạn muốn đọc theo từng dòng (ví dụ, để phân tích log), hãy dùng stream.
Ví dụ: đọc tệp theo dòng bằng BufferedReader
import java.nio.file.*;
import java.io.*;
public class ReadLinesExample {
public static void main(String[] args) throws IOException {
Path path = Paths.get("myfile.txt");
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println("Dòng: " + line);
}
}
}
}
Ví dụ: ghi tệp theo dòng bằng BufferedWriter và bổ sung
import java.nio.file.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class WriteAppendExample {
public static void main(String[] args) throws IOException {
Path path = Paths.get("myfile.txt");
try (BufferedWriter writer = Files.newBufferedWriter(
path,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND)) {
writer.write("Thêm một dòng nữa qua BufferedWriter!");
writer.newLine();
}
}
}
6. Thực hành: tạo tệp, bổ sung các dòng vào đó
Hãy ghép mọi thứ vào một ứng dụng. Giả sử ta có chương trình quản lý danh sách việc cần làm (todo-list) trong một tệp văn bản. Mỗi lần người dùng thêm một công việc mới, ta sẽ bổ sung nó vào cuối tệp.
import java.nio.file.*;
import java.io.IOException;
import java.util.List;
import java.util.Scanner;
public class TodoList {
public static void main(String[] args) throws IOException {
Path path = Paths.get("todo.txt");
Scanner scanner = new Scanner(System.in);
System.out.print("Nhập công việc mới: ");
String task = scanner.nextLine();
// Bổ sung công việc vào cuối tệp (tạo tệp nếu chưa có)
Files.write(path,
List.of(task),
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
System.out.println("Đã thêm công việc!");
}
}
Hãy chạy chương trình vài lần — mỗi công việc sẽ xuất hiện trong tệp trên một dòng mới. Vậy là ta đã có “todo-list” đầu tiên mang tính “vĩnh viễn”!
7. Xử lý lỗi: có thể gặp vấn đề gì?
Làm việc với tệp luôn tiềm ẩn rủi ro lỗi. Có thể xảy ra những trường hợp sau:
- IOException — ngoại lệ cơ bản cho mọi lỗi nhập/xuất. Có thể phát sinh nếu tệp đang bị chương trình khác sử dụng, không có quyền ghi/đọc, đầy đĩa, v.v.
- NoSuchFileException — khi bạn cố đọc hoặc bổ sung vào một tệp không tồn tại (và bạn không chỉ định tùy chọn CREATE).
- FileAlreadyExistsException — khi bạn dùng tùy chọn CREATE_NEW nhưng tệp đã tồn tại.
Khuyến nghị: luôn bao bọc thao tác với tệp trong try-catch:
try {
// các thao tác với tệp của bạn
} catch (IOException e) {
System.out.println("Lỗi khi làm việc với tệp: " + e.getMessage());
}
8. Những lỗi thường gặp khi làm việc với chế độ mở tệp
Lỗi số 1: quên chỉ định tùy chọn CREATE khi bổ sung. Nếu bạn cố bổ sung (APPEND) vào một tệp chưa tồn tại, sẽ nhận NoSuchFileException. Luôn thêm CREATE nếu bạn muốn tệp được tạo tự động.
Lỗi số 2: vô tình ghi đè tệp. Gọi Files.write(path, data) mà không kèm tùy chọn bổ sung sẽ xóa toàn bộ nội dung cũ của tệp. Nếu bạn muốn thêm dữ liệu thay vì xóa dữ liệu cũ, hãy dùng APPEND.
Lỗi số 3: cố gắng bổ sung dữ liệu nhị phân vào tệp văn bản (hoặc ngược lại). Nếu bạn trộn dữ liệu văn bản và nhị phân trong cùng một tệp, nhiều khả năng tệp sẽ không thể đọc được. Luôn giữ một định dạng cho một tệp.
Lỗi số 4: quên đóng stream. Nếu dùng stream (BufferedWriter, BufferedReader), đừng quên đóng chúng (tốt nhất bằng try-with-resources), nếu không tệp có thể bị khóa.
Lỗi số 5: không xử lý ngoại lệ. Bất kỳ thao tác nào với tệp cũng có thể ném IOException. Nếu không xử lý, chương trình sẽ kết thúc đột ngột.
GO TO FULL VERSION