Buổi 8: Dòng dữ liệu vào/ra và tệp
Hãy hình dung bạn làm phần mềm cho thư viện khoa: mỗi ngày có hàng trăm lượt mượn sách, danh sách sinh viên, điểm danh, nhật ký truy cập. Nếu tất cả chỉ nằm trong biến và mảng thì tắt chương trình là mất sạch. Muốn dữ liệu sống lâu hơn một lần chạy, bạn phải ghi ra tệp và đọc lại khi cần. Muốn chương trình đọc nhanh hàng trăm nghìn dòng đầu vào của một bài thi lập trình, bạn phải biết cách đọc stdin hiệu quả.
Trong Java, mọi chuyện "đưa dữ liệu vào" và "đưa dữ liệu ra" — bàn phím, màn hình, tệp, mạng — đều được mô hình hóa bằng một khái niệm thống nhất: dòng dữ liệu (stream). Hiểu một lần, bạn dùng được cho mọi nguồn. Buổi này ta đi từ khái niệm, qua vào/ra nhị phân và văn bản, ghi cả đối tượng, thao tác với tệp và thư mục, rồi kết thúc bằng kỹ thuật đọc stdin nhanh mà bạn sẽ dùng thường xuyên khi làm bài trên Codelab.
Mục tiêu buổi học
- Sau buổi này bạn có thể phân biệt dòng byte với dòng ký tự và chọn đúng lớp cho từng loại dữ liệu.
- Bạn có thể ghi và đọc dữ liệu nhị phân (số, chuỗi, logic) bằng
DataOutputStream/DataInputStreamvà sao chép tệp bất kỳ bằng bộ đệm. - Bạn có thể ghi và đọc tệp văn bản bằng
Reader/Writer,BufferedReader,PrintWriter,Scanner,Files, luôn chỉ rõ bảng mã UTF-8. - Bạn có thể lưu và khôi phục đối tượng bằng
SerializablevàObjectOutputStream/ObjectInputStream. - Bạn có thể dùng
File,Path,Filesđể kiểm tra, tạo, liệt kê, sao chép và xóa tệp/thư mục. - Bạn luôn đóng tài nguyên đúng cách bằng try-with-resources và đọc stdin nhanh bằng
BufferedReader,StringTokenizer,StreamTokenizer.
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1. Dòng dữ liệu | Byte vs ký tự, bốn lớp gốc, decorator | 15 phút |
| 2. try-with-resources | Vì sao phải đóng tài nguyên, cú pháp, thứ tự đóng | 10 phút |
| 3. Vào/ra nhị phân | FileInputStream/FileOutputStream, Buffered, Data stream + bài tập 8.1 | 25 phút |
| 4. Vào/ra văn bản | FileWriter/FileReader, Buffered, PrintWriter, Scanner, UTF-8, Files + bài tập 8.2 | 35 phút |
| 5. Vào/ra đối tượng | Serializable, ObjectOutputStream/ObjectInputStream, transient + bài tập 8.3 | 20 phút |
| 6. File, Path, Files | Kiểm tra, tạo, liệt kê, xóa, đường dẫn | 15 phút |
| 7. Sao chép tệp | Sao chép bằng bộ đệm, Files.copy, tách và gộp + bài tập 8.4 | 25 phút |
| 8. Đọc stdin nhanh | BufferedReader, StringTokenizer, StreamTokenizer, EOF, gom output + bài tập 8.5 | 35 phút |
Tổng cộng 180 phút, trong đó thực hành chiếm khoảng 55%.
1. Dòng dữ liệu: byte và ký tự
Dòng dữ liệu là gì?
Dòng (stream) là một "ống dẫn" tuần tự: dữ liệu chảy từ nguồn tới đích, bạn chỉ đọc hoặc ghi theo thứ tự từ đầu tới cuối, không quay lại tùy ý. Chương trình của bạn đứng ở giữa:
- Dòng vào (input): dữ liệu chảy từ bên ngoài (bàn phím, tệp, mạng) vào chương trình — bạn đọc.
- Dòng ra (output): dữ liệu chảy từ chương trình ra bên ngoài (màn hình, tệp, mạng) — bạn ghi.
Bạn đã dùng dòng từ buổi 1: System.in là dòng vào chuẩn, System.out là dòng ra chuẩn. Điểm hay của mô hình này là mã đọc từ tệp trông giống hệt mã đọc từ bàn phím — chỉ khác chỗ bạn tạo ra dòng.
Hai họ lớp: byte và ký tự
Máy tính lưu mọi thứ là byte. Nhưng có hai cách nhìn dữ liệu, nên Java có hai họ lớp tách biệt:
- Họ byte (
InputStream/OutputStream): đơn vị là 1 byte (0–255). Dùng cho dữ liệu nhị phân: ảnh, âm thanh, tệp nén, tệp.dat, hay bất cứ thứ gì không phải chữ. - Họ ký tự (
Reader/Writer): đơn vị là 1 ký tự Unicode (char). Dùng cho văn bản. Họ này lo việc chuyển byte ↔ ký tự theo một bảng mã (UTF-8, UTF-16…).
Luồng byte (byte stream)
8 bit/phần tử — mọi loại dữ liệu: ảnh, âm thanh, .class
- FileInputStream📄 nối với fileđọc byte từ file
- BufferedInputStream🎁 decoratorđệm → nhanh hơn
- DataInputStream🎁 decoratorreadInt(), readUTF()
- ObjectInputStream🎁 decoratorreadObject()
- FileOutputStream📄 nối với fileghi byte ra file
- BufferedOutputStream🎁 decoratorđệm → nhanh hơn
- DataOutputStream🎁 decoratorwriteInt(), writeUTF()
- ObjectOutputStream🎁 decoratorwriteObject()
Luồng ký tự (character stream)
16 bit/ký tự (Unicode) — chuyên cho văn bản
- InputStreamReader🌉 cầu nốibyte → char (charset)
- FileReader📄 nối với fileđọc ký tự từ file
- BufferedReader🎁 decoratorreadLine()
- StringReader📄 nối với fileđọc từ String
- OutputStreamWriter🌉 cầu nốichar → byte (charset)
- FileWriter📄 nối với fileghi ký tự ra file
- BufferedWriter🎁 decoratornewLine()
- PrintWriter🎁 decoratorprintln(), printf()
🎁 Decorator: bọc nhiều lớp, dữ liệu chảy qua từng lớp
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
DataOutputStream out = new DataOutputStream(new FileOutputStream("n.bin"));
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("s.dat"));
Chọn luồng byte cho dữ liệu nhị phân, luồng ký tự cho văn bản. Mỗi lớp decorator bọc lớp trong để thêm khả năng (đệm, đọc/ghi kiểu dữ liệu, serialize) — dữ liệu chảy qua từng lớp.
Hình trên cho thấy hai họ song song và các lớp "bọc" (Buffered…) chồng lên lớp nền. Hãy đọc từ dưới lên: lớp nền nói chuyện trực tiếp với tệp, lớp bọc thêm bộ đệm hoặc thêm khả năng đọc kiểu dữ liệu cao hơn.
| Họ | Đọc | Ghi | Đơn vị | Khi nào dùng |
|---|---|---|---|---|
| Byte | InputStream |
OutputStream |
byte (0–255) |
Ảnh, tệp nhị phân, sao chép tệp |
| Ký tự | Reader |
Writer |
char |
Văn bản, CSV, log, cấu hình |
Bốn lớp trên đều là lớp trừu tượng; bạn dùng các lớp con cụ thể như FileInputStream, FileReader, ByteArrayInputStream… Cầu nối giữa hai họ là InputStreamReader và OutputStreamWriter: chúng nhận một dòng byte cùng một bảng mã và cho ra dòng ký tự.
Byte khác ký tự như thế nào?
Một ký tự không nhất thiết là một byte. Trong UTF-8, chữ cái ASCII chiếm 1 byte nhưng chữ có dấu tiếng Việt chiếm 2 hoặc 3 byte. Ví dụ dưới đây đếm số ký tự và số byte của cùng một chuỗi:
String s = "Chào";
byte[] b = s.getBytes(java.nio.charset.StandardCharsets.UTF_8);
System.out.println("Số ký tự: " + s.length());
System.out.println("Số byte UTF-8: " + b.length);
for (byte x : b) System.out.print((x & 0xFF) + " ");
System.out.println();Chuỗi "Chào" có 4 ký tự nhưng 5 byte, vì chữ à được mã hóa bằng hai byte (195 và 160). Phép x & 0xFF đổi byte có dấu (−128..127) về giá trị 0..255 để in cho dễ nhìn. Đây là lý do không được đọc tệp văn bản tiếng Việt bằng dòng byte rồi ép từng byte thành char: bạn sẽ ra chữ vỡ.
Lưu ý: Quy tắc chọn nhanh: dữ liệu "người đọc được" (văn bản) →
Reader/Writer; dữ liệu "máy đọc" (nhị phân) →InputStream/OutputStream.
Phương thức read() trả về int
Mọi InputStream đều có int read(): trả về một byte dưới dạng số 0–255, hoặc −1 khi hết dữ liệu. Vì cần dành giá trị −1 làm tín hiệu "hết", kiểu trả về là int chứ không phải byte. Vòng lặp đọc kinh điển:
byte[] data = {72, 105, 33};
InputStream in = new ByteArrayInputStream(data);
int b;
while ((b = in.read()) != -1) {
System.out.print((char) b);
}
System.out.println();ByteArrayInputStream là dòng vào lấy dữ liệu từ một mảng byte trong bộ nhớ — rất tiện để thử vì không cần tệp thật. Cách viết (b = in.read()) != -1 vừa gán vừa kiểm tra, bạn sẽ gặp lại ở hầu hết mã vào/ra.
Decorator: xếp chồng các lớp bọc
Các lớp I/O của Java theo mẫu thiết kế Decorator: một dòng có thể được "bọc" bằng dòng khác để thêm chức năng, ví dụ:
FileInputStream → BufferedInputStream → DataInputStream
(đọc byte thô) (thêm bộ đệm) (đọc int, double, UTF…)Mỗi lớp bọc nhận dòng bên trong qua hàm tạo. Bạn chỉ cần thao tác với lớp ngoài cùng; đóng lớp ngoài cùng sẽ đóng luôn các lớp bên trong.
Ví dụ thực tế: Giống như lắp bộ lọc lên vòi nước: vòi (tệp) cho nước chảy ra, bộ lọc
Bufferedlàm dòng chảy êm hơn, đầu phunDatađịnh hình dòng nước thành những vòi có ý nghĩa (số, chuỗi). Bạn thêm hoặc bớt bộ lọc mà không phải đổi vòi.
2. Đóng tài nguyên đúng cách: try-with-resources
Tệp, kết nối mạng, cơ sở dữ liệu là tài nguyên của hệ điều hành, có giới hạn. Mở mà không đóng thì tệp có thể bị khóa, dữ liệu ghi trong bộ đệm chưa kịp xuống đĩa, và chương trình chạy lâu sẽ cạn "số lượng tệp được mở". Cách làm cũ dùng finally rất dài dòng và dễ sai:
// Cách cũ — dài và dễ quên
FileInputStream in = null;
try {
in = new FileInputStream("data.bin");
// ... đọc ...
} finally {
if (in != null) in.close(); // close() cũng có thể ném IOException!
}Từ Java 7, try-with-resources làm việc này tự động. Bạn khai báo tài nguyên trong dấu ngoặc tròn sau try; khi khối kết thúc (bình thường hoặc do lỗi), Java tự gọi close() theo thứ tự ngược với thứ tự mở. Điều kiện: kiểu tài nguyên phải cài đặt AutoCloseable (mọi dòng vào/ra đều thỏa).
try – catch – finally chạy thế nào?
Chọn tình huống, theo dõi đường đi và thứ tự in ra consoletry {int r = 10 / 2;System.out.println("Kết quả: " + r);} catch (ArithmeticException e) {System.out.println("Chia cho 0! " + e.getMessage());} finally {System.out.println("Dọn dẹp");}System.out.println("Tiếp tục");
Mô phỏng phía trên (đã gặp ở buổi 4) nhắc lại đường đi qua try/catch/finally. Với try-with-resources, bước "đóng tài nguyên" được chèn tự động trước khi catch chạy. Ví dụ sau tự định nghĩa một tài nguyên để bạn thấy rõ thứ tự:
class Res implements AutoCloseable {
private final String name;
Res(String name) { this.name = name; System.out.println("mở " + name); }
public void close() { System.out.println("đóng " + name); }
}
public class Main {
public static void main(String[] args) {
try (Res a = new Res("A"); Res b = new Res("B")) {
System.out.println("đang dùng A và B");
throw new IllegalStateException("lỗi giữa chừng");
} catch (IllegalStateException e) {
System.out.println("bắt: " + e.getMessage());
}
}
}Quan sát kết quả: mở A, mở B, dùng, đóng B rồi mới đóng A, và chỉ sau đó catch mới in dòng "bắt". Tài nguyên mở sau được đóng trước (như xếp chồng đĩa). Dù có ngoại lệ, cả hai vẫn được đóng.
Mẹo: Từ đây trở đi, mọi ví dụ mở tệp trong bài đều dùng try-with-resources. Hãy tập thói quen: thấy
new FileXxx(...)hoặcnew ...Stream(...)thì đặt nó trong ngoặc củatry.
Cảnh báo: Với dòng ghi, quên đóng nghĩa là dữ liệu còn nằm trong bộ đệm chưa xuống tệp. Đọc lại tệp ngay sau đó có thể thấy tệp rỗng hoặc cụt. Đây là lỗi rất hay gặp khi làm bài tập ghi rồi đọc lại.
Một lưu ý nhỏ về cú pháp: sau try (...) bạn có thể có catch và/hoặc finally như bình thường, hoặc không có gì cả (khi đó ngoại lệ checked phải khai báo bằng throws hoặc bắt ở nơi khác). Trong các ví dụ chạy thử của bài, chương trình được bọc sẵn throws Exception nên bạn không phải viết catch ở mỗi nơi.
3. Vào/ra nhị phân
FileOutputStream và FileInputStream
FileOutputStream ghi byte ra tệp, FileInputStream đọc byte từ tệp. Mặc định FileOutputStream ghi đè tệp cũ (tạo mới nếu chưa có); muốn nối thêm vào cuối, truyền true làm tham số thứ hai: new FileOutputStream("a.bin", true). Phương thức write(int) chỉ ghi 8 bit thấp của số nguyên nhận vào.
try (FileOutputStream out = new FileOutputStream("demo.bin")) {
for (int i = 250; i < 260; i++) out.write(i); // chỉ 8 bit thấp được ghi
}
try (FileInputStream in = new FileInputStream("demo.bin")) {
int b;
while ((b = in.read()) != -1) System.out.print(b + " ");
}
System.out.println();
System.out.println("Kích thước: " + new File("demo.bin").length() + " byte");Ta ghi 10 số từ 250 đến 259 nhưng đọc ra 250 … 255 0 1 2 3: vì mỗi byte chỉ chứa 0–255, số 256 bị cắt còn 0 (tràn, giống phần ép kiểu ở buổi 1). Tệp có đúng 10 byte, mỗi lần write đúng một byte.
Ghi/đọc theo khối: mảng byte và bộ đệm
Gọi read()/write() từng byte là rất chậm vì mỗi lần là một "chuyến đi" xuống hệ điều hành. Cách tốt hơn: đọc/ghi cả khối bằng mảng byte — int read(byte[] buf) trả về số byte thực đọc được (hoặc −1 khi hết) và write(byte[] buf, int off, int len). Cách thứ hai: bọc bằng BufferedInputStream / BufferedOutputStream, các lớp này tự gom byte vào một bộ đệm (mặc định 8 KB) rồi mới làm việc với đĩa.
| Cách | Số lần chạm vào đĩa | Khi nào dùng |
|---|---|---|
read() từng byte, không đệm |
Rất nhiều | Chỉ để học |
read() từng byte qua Buffered… |
Ít (theo khối 8 KB) | Dữ liệu nhỏ, mã đơn giản |
read(byte[]) với mảng 4–8 KB |
Ít | Sao chép tệp, xử lý khối |
Mẹo: Hầu như luôn bọc dòng tệp bằng
Buffered…. Chi phí thêm một lớp bọc gần như bằng không, nhưng tốc độ có thể nhanh hơn hàng chục lần với tệp lớn.
DataOutputStream và DataInputStream
Ghi số thật, chuỗi, logic bằng từng byte thì rất khổ: một int là 4 byte, một double là 8 byte. DataOutputStream / DataInputStream là lớp bọc giúp bạn ghi và đọc trực tiếp các kiểu nguyên thủy (writeInt, writeDouble, writeBoolean, writeLong, writeUTF…) theo định dạng nhị phân chuẩn, độc lập nền tảng.
Ví dụ sau đọc n và n số nguyên từ stdin, ghi vào tệp nhị phân (có n đứng đầu tệp để khi đọc lại biết có bao nhiêu số), rồi đọc ra và tính tổng:
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new FileOutputStream("nums.dat")))) {
out.writeInt(n);
for (int i = 0; i < n; i++) out.writeInt(sc.nextInt());
}
System.out.println("Kích thước tệp: " + new File("nums.dat").length() + " byte");
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new FileInputStream("nums.dat")))) {
int m = in.readInt();
long sum = 0;
for (int i = 0; i < m; i++) sum += in.readInt();
System.out.println("Đọc lại " + m + " số, tổng = " + sum);
}Tệp có 4 số int (một số n và ba giá trị) nên dài 16 byte. Chú ý cách ta lồng ba lớp: tệp → bộ đệm → Data. Dữ liệu nhị phân không thể mở bằng trình soạn thảo văn bản để đọc, nhưng nhỏ gọn và đọc lại chính xác tuyệt đối (không mất độ chính xác khi lưu double như khi lưu thành chữ).
Quy tắc vàng: đọc đúng thứ tự và đúng kiểu đã ghi
Tệp nhị phân không tự mô tả: nó chỉ là một dãy byte. Bạn phải đọc ra đúng thứ tự và đúng kiểu như lúc ghi. Ghi writeInt rồi đọc readDouble sẽ ra rác. Khi đọc quá cuối tệp, Java ném EOFException:
try (DataOutputStream out = new DataOutputStream(new FileOutputStream("rec.dat"))) {
out.writeUTF("Nguyễn An");
out.writeInt(2005);
out.writeDouble(8.5);
out.writeBoolean(true);
}
try (DataInputStream in = new DataInputStream(new FileInputStream("rec.dat"))) {
String name = in.readUTF();
int year = in.readInt();
double gpa = in.readDouble();
boolean ok = in.readBoolean();
System.out.println(name + " | " + year + " | " + gpa + " | " + ok);
in.readInt(); // không còn dữ liệu
} catch (EOFException e) {
System.out.println("Hết tệp: EOFException");
}writeUTF ghi chuỗi kèm độ dài ở đầu nên readUTF biết dừng ở đâu. Với dữ liệu có số lượng không biết trước, bạn có hai cách: ghi trước số lượng (như ví dụ nums.dat), hoặc đọc cho đến khi bắt EOFException. Cách ghi số lượng trước gọn và an toàn hơn.
| Kiểu | Phương thức ghi | Phương thức đọc | Số byte |
|---|---|---|---|
int |
writeInt |
readInt |
4 |
long |
writeLong |
readLong |
8 |
double |
writeDouble |
readDouble |
8 |
boolean |
writeBoolean |
readBoolean |
1 |
String |
writeUTF |
readUTF |
2 + độ dài UTF-8 |
Cảnh báo:
DataInputStream.read()(không đuôi) trả về byte thô và −1 khi hết — khác vớireadInt(),readByte()… vốn némEOFException. Đừng dùng lẫn lộn hai kiểu để kiểm tra "hết tệp".
Ví dụ thực tế: Một trò chơi lưu điểm cao: tệp
highscore.datchứa lần lượt tên người chơi (writeUTF), điểm (writeInt), thời gian chơi (writeLong). Mở bằng chính chương trình đó, đọc đúng thứ tự là khôi phục được.
Bạn vừa học xong vào/ra nhị phân, giờ củng cố bằng ba bài ghi tệp nhị phân rồi đọc lại, kể cả một bài mã hóa XOR từng byte.
4. Vào/ra văn bản
Reader, Writer và vấn đề bảng mã
Với văn bản, bạn dùng họ Reader/Writer. Các lớp gần gũi nhất:
| Lớp | Vai trò |
|---|---|
FileWriter / FileReader |
Ghi/đọc ký tự thẳng từ tệp (dùng bảng mã mặc định của máy) |
BufferedWriter / BufferedReader |
Thêm bộ đệm; BufferedReader có readLine(), BufferedWriter có newLine() |
PrintWriter |
Ghi bằng print, println, printf quen thuộc như System.out |
OutputStreamWriter / InputStreamReader |
Cầu nối byte → ký tự, cho phép chỉ rõ bảng mã |
Cảnh báo:
FileWriter("a.txt")vàFileReader("a.txt")dùng bảng mã mặc định của hệ điều hành, có thể khác nhau giữa máy bạn, máy bạn bè và máy chấm bài. Kết quả: tiếng Việt thành dấu?. Với tệp có chữ có dấu, luôn dùngOutputStreamWriter/InputStreamReader(hoặcFiles) kèmStandardCharsets.UTF_8. Ngay cảSystem.incũng nên được bọc với UTF-8 như dưới đây. HằngStandardCharsets.UTF_8nằm trong góijava.nio.charsetnên cần dòngimport java.nio.charset.StandardCharsets;ở đầu tệp (các ví dụ bên dưới đã có sẵn).
Ghi và đọc từng dòng
Mẫu mã quan trọng nhất của vào/ra văn bản: đọc từng dòng bằng readLine() (trả về null khi hết) và ghi từng dòng bằng write + newLine(). Ví dụ đọc các dòng từ stdin cho đến hết, lưu vào tệp, rồi đọc lại tệp và đánh số dòng:
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
try (BufferedReader in = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
BufferedWriter out = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("note.txt"), StandardCharsets.UTF_8))) {
String line;
while ((line = in.readLine()) != null) {
out.write(line);
out.newLine();
}
}
try (BufferedReader r = new BufferedReader(
new InputStreamReader(new FileInputStream("note.txt"), StandardCharsets.UTF_8))) {
int n = 0;
String line;
while ((line = r.readLine()) != null) {
System.out.println(++n + ": " + line);
}
}
}
}Hai tài nguyên trong cùng một try (…; …) được đóng ngược thứ tự. readLine() không trả về ký tự xuống dòng, nên khi ghi lại bạn phải tự newLine() (hàm này dùng ký hiệu xuống dòng đúng với hệ điều hành). Vòng while ((line = r.readLine()) != null) tương tự vòng đọc byte ở phần 1, chỉ khác tín hiệu hết là null thay vì −1.
Nối thêm vào cuối tệp (append)
Mặc định ghi sẽ xóa nội dung cũ. Để nối vào cuối, truyền true ở tham số thứ hai của FileOutputStream (hoặc FileWriter). Cách này dùng cho nhật ký (log): mỗi sự kiện thêm một dòng, không mất lịch sử.
String[] events = {"dang nhap", "xem diem", "dang xuat"};
for (String e : events) {
// mỗi lần mở ở chế độ append rồi đóng
try (PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter("log.txt", true)))) {
pw.println("[sự kiện] " + e);
}
}
try (BufferedReader r = new BufferedReader(new FileReader("log.txt"))) {
long count = r.lines().count();
System.out.println("Số dòng log: " + count);
}PrintWriter rất tiện vì có println và printf như System.out. Chú ý: PrintWriter nuốt ngoại lệ (không ném IOException), nên nếu cần chắc chắn ghi thành công bạn gọi checkError() hoặc dùng BufferedWriter. Chuỗi trong ví dụ có tiếng Việt nhưng log chỉ đếm dòng nên không ảnh hưởng.
Lưu ý: Nếu chạy ví dụ trên hai lần trong cùng thư mục, tệp
log.txtsẽ có 6 dòng chứ không phải 3, vì append cộng dồn. Trên Codelab mỗi lần chạy có thư mục riêng nên luôn thấy 3.
Tiếng Việt và UTF-8
Hãy xem số ký tự và số byte khác nhau thế nào khi ghi tiếng Việt bằng UTF-8, và kiểm tra rằng đọc lại đúng chữ:
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
String text = "Trường Công nghệ";
try (Writer w = new OutputStreamWriter(new FileOutputStream("vn.txt"), StandardCharsets.UTF_8)) {
w.write(text);
}
System.out.println("Số ký tự: " + text.length());
System.out.println("Số byte trong tệp: " + new File("vn.txt").length());
try (Reader r = new InputStreamReader(new FileInputStream("vn.txt"), StandardCharsets.UTF_8)) {
char[] buf = new char[100];
int n = r.read(buf);
String back = new String(buf, 0, n);
System.out.println("Đọc lại: " + back + " (khớp: " + back.equals(text) + ")");
}
}
}Số byte lớn hơn số ký tự vì các chữ ư, ờ, ô, ệ chiếm nhiều hơn một byte. Reader.read(char[]) giống phiên bản byte[]: trả về số ký tự đọc được. Quy tắc: ghi bằng bảng mã nào thì đọc bằng đúng bảng mã đó; trong khóa học này luôn dùng UTF-8.
Scanner đọc từ tệp
Bạn đã dùng Scanner(System.in). Scanner cũng đọc được từ File, rất hợp với tệp có nhiều số và chữ lẫn lộn vì tự tách theo khoảng trắng và đổi kiểu: nextInt, nextDouble, next, nextLine, hasNext….
try (PrintWriter pw = new PrintWriter("scores.txt", "UTF-8")) {
pw.println("An 8.5");
pw.println("Binh 7.0");
pw.println("Chi 9.5");
}
try (Scanner sc = new Scanner(new File("scores.txt"), "UTF-8")) {
double sum = 0;
int n = 0;
while (sc.hasNext()) {
String name = sc.next();
double score = sc.nextDouble();
sum += score;
n++;
System.out.println(name + " -> " + score);
}
System.out.printf("Trung bình: %.2f%n", sum / n);
}Dòng new PrintWriter("scores.txt", "UTF-8") là cách ngắn nhất để ghi văn bản có chỉ rõ bảng mã. hasNext() kiểm tra còn "từ" nào chưa đọc, nên vòng lặp tự dừng ở cuối tệp. Nhược điểm của Scanner là chậm với dữ liệu lớn (nó phân tích bằng biểu thức chính quy) — ta sẽ so sánh ở phần 8.
Lớp tiện ích Files (Java 7+)
Với tệp không quá lớn, lớp java.nio.file.Files cho phép đọc/ghi chỉ bằng một dòng. Nó tự mở và đóng tệp giúp bạn, nên không cần try-with-resources:
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
Path p = Paths.get("sv.txt");
Files.write(p, Arrays.asList("An 8.5", "Binh 7.0", "Chi 9.0"), StandardCharsets.UTF_8);
List<String> lines = Files.readAllLines(p, StandardCharsets.UTF_8);
double sum = 0;
for (String line : lines) {
sum += Double.parseDouble(line.split(" ")[1]);
}
System.out.println("Số dòng: " + lines.size());
System.out.println("Tổng điểm: " + sum);
long longLines = Files.lines(p, StandardCharsets.UTF_8).filter(s -> s.length() > 6).count();
System.out.println("Dòng dài hơn 6 ký tự: " + longLines);
}
}Files.lines trả về một Stream (buổi 10) nên có thể filter, count… trực tiếp. Lưu ý: readAllLines đọc toàn bộ tệp lên bộ nhớ; với tệp hàng trăm MB hãy đọc từng dòng bằng BufferedReader hoặc Files.newBufferedReader.
Mẹo: Quy tắc chọn: tệp nhỏ và cần nhanh gọn →
Files.readAllLines/Files.write; tệp lớn hoặc đọc theo luồng →BufferedReader; dữ liệu có cấu trúc số/chữ pha trộn nhỏ →Scanner.
| Nhu cầu | Cách ngắn gọn | Cách tiết kiệm bộ nhớ |
|---|---|---|
| Đọc toàn bộ dòng | Files.readAllLines(p, UTF_8) |
BufferedReader.readLine() |
| Ghi nhiều dòng | Files.write(p, list, UTF_8) |
BufferedWriter + newLine() |
| Đọc cả tệp thành chuỗi | new String(Files.readAllBytes(p), UTF_8) |
Đọc theo khối char[] |
| Nối vào cuối | Files.write(p, bytes, StandardOpenOption.APPEND) |
FileWriter(name, true) |
Phần văn bản là phần dùng nhiều nhất trong thực tế, hãy luyện thật kỹ qua các bài đếm, đảo, tìm trong tệp văn bản.
5. Vào/ra đối tượng: Serializable
Tuần tự hóa (serialization)
Với dữ liệu là một đối tượng (sinh viên, cuốn sách…), ghi từng trường bằng Data…Stream thì phải tự nhớ thứ tự và khi thêm một trường là sửa nhiều chỗ. Java cho phép tuần tự hóa: biến cả đối tượng (cùng các đối tượng nó tham chiếu) thành một dãy byte rồi ghi ra dòng; ngược lại gọi là giải tuần tự (deserialization). Hai lớp thực hiện: ObjectOutputStream.writeObject(Object) và ObjectInputStream.readObject().
Điều kiện: lớp phải cài đặt giao diện đánh dấu java.io.Serializable (không có phương thức nào để viết). Các thành phần là đối tượng bên trong cũng phải tuần tự hóa được.
class Student implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
double gpa;
transient String password; // transient: KHÔNG được ghi
Student(String name, int age, double gpa, String password) {
this.name = name; this.age = age; this.gpa = gpa; this.password = password;
}
public String toString() { return name + " (" + age + ") gpa=" + gpa + " pw=" + password; }
}
public class Main {
public static void main(String[] args) throws Exception {
List<Student> list = new ArrayList<>();
list.add(new Student("An", 20, 8.5, "abc123"));
list.add(new Student("Binh", 21, 7.25, "xyz789"));
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("sv.ser")))) {
out.writeObject(list);
}
try (ObjectInputStream in = new ObjectInputStream(
new BufferedInputStream(new FileInputStream("sv.ser")))) {
List<Student> back = (List<Student>) in.readObject();
for (Student s : back) System.out.println(s);
}
}
}Một lệnh writeObject(list) ghi cả danh sách lẫn mọi Student bên trong. Khi đọc lại, readObject() trả về Object, bạn ép kiểu về đúng kiểu đã ghi (trình biên dịch có thể cảnh báo "unchecked" với List<…>; ở đây chấp nhận được). Điều đặc biệt: trường password đánh dấu transient nên không được lưu, và khi đọc lại nó mang giá trị mặc định null. Dùng transient cho dữ liệu nhạy cảm hoặc dữ liệu có thể tính lại (bộ nhớ đệm, kết nối).
Điều cần nhớ về tuần tự hóa
| Chủ đề | Quy tắc |
|---|---|
| Điều kiện | Lớp (và các đối tượng thành phần) phải implements Serializable |
transient |
Trường không được ghi; đọc lại là mặc định (null, 0, false) |
static |
Thuộc lớp, không thuộc đối tượng nên không được ghi |
serialVersionUID |
Số phiên bản; khai báo rõ để tránh InvalidClassException khi sửa lớp |
| Thứ tự đọc | Đọc đúng thứ tự các writeObject đã gọi |
| Kế thừa | Lớp con của lớp Serializable cũng serializable; lớp cha không Serializable cần có hàm tạo không đối |
Nếu bạn quên implements Serializable, writeObject ném NotSerializableException. Ví dụ sau bắt lỗi đó để bạn thấy thông báo:
class Plain { int x = 5; }
public class Main {
public static void main(String[] args) throws Exception {
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("p.ser"))) {
out.writeObject(new Plain());
} catch (NotSerializableException e) {
System.out.println("Không tuần tự hóa được: " + e.getMessage());
}
}
}Lưu ý: Tệp
.serlà định dạng nhị phân riêng của Java: chỉ chương trình Java đọc được và không nên đọc dữ liệu từ nguồn không tin cậy (giải tuần tự dữ liệu độc hại là lỗ hổng bảo mật nổi tiếng). Với dữ liệu trao đổi giữa các hệ thống, người ta dùng JSON/XML/CSV thay vìSerializable.
Ví dụ thực tế: Ứng dụng quản lý thư viện nhỏ lưu cả
Listcác cuốn sách bằng một lệnhwriteObjectkhi đóng chương trình, và một lệnhreadObjectkhi mở lại — đủ cho ứng dụng cá nhân không cần cơ sở dữ liệu.
Hãy thử ghi và đọc đối tượng sinh viên rồi đếm số người đạt theo điểm:
6. File, Path và Files: làm việc với tệp và thư mục
Đến giờ ta chỉ đọc/ghi nội dung tệp. Còn việc hỏi "tệp có tồn tại không, dài bao nhiêu, nằm ở thư mục nào, liệt kê thư mục, xóa tệp" cần các lớp quản lý đường dẫn. Java có hai thế hệ:
| Thế hệ | Lớp | Ghi chú |
|---|---|---|
| Cũ (Java 1.0) | java.io.File |
Quen thuộc, nhiều tài liệu dùng; nhiều phương thức chỉ trả false khi lỗi, ít thông tin |
| Mới (Java 7+) | java.nio.file.Path, Paths, Files |
Ném ngoại lệ rõ ràng, nhiều tính năng (sao chép, duyệt cây thư mục), nên ưu tiên cho mã mới |
Lớp File
Một đối tượng File chỉ đại diện cho một đường dẫn; tạo nó chưa chạm vào đĩa. Các phương thức hay dùng:
File dir = new File("baocao");
dir.mkdir(); // tạo thư mục (nếu chưa có)
File f = new File(dir, "ketqua.txt"); // tệp nằm trong thư mục
System.out.println("Tồn tại trước khi ghi? " + f.exists());
try (PrintWriter pw = new PrintWriter(f, "UTF-8")) {
pw.println("điểm trung bình: 8.25");
}
System.out.println("Tồn tại sau khi ghi? " + f.exists());
System.out.println("Là tệp? " + f.isFile() + ", là thư mục? " + f.isDirectory());
System.out.println("Tên: " + f.getName());
System.out.println("Kích thước: " + f.length() + " byte");
System.out.println("Xóa tệp được? " + f.delete());
System.out.println("Xóa thư mục được? " + dir.delete());Thư mục chỉ xóa được khi rỗng, nên ta xóa tệp trước rồi mới xóa thư mục. Các phương thức khác: getParent(), getAbsolutePath(), canRead(), createNewFile(), mkdirs() (tạo cả chuỗi thư mục cha) và listFiles() / list() để liệt kê.
Liệt kê thư mục
File.list() trả về mảng tên nhưng không đảm bảo thứ tự; nếu cần đầu ra ổn định hãy sắp xếp trước khi in:
File dir = new File("lab");
dir.mkdir();
for (String n : new String[]{"b.txt", "a.txt", "c.dat"}) {
new File(dir, n).createNewFile();
}
String[] names = dir.list();
Arrays.sort(names);
for (String n : names) {
System.out.println(n + (n.endsWith(".txt") ? " (văn bản)" : ""));
}
for (File f : dir.listFiles()) f.delete();
dir.delete();Path và Files
Path biểu diễn đường dẫn dưới dạng đối tượng bất biến, có nhiều phép thao tác tiện: ghép (resolve), lấy tên (getFileName), lấy thư mục cha (getParent), chuẩn hóa .. và . (normalize). Còn Files chứa các phương thức tĩnh làm việc với đĩa.
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
Path p = Paths.get("du-lieu", "2024", "diem.csv");
System.out.println("Đường dẫn: " + p);
System.out.println("Tên tệp: " + p.getFileName());
System.out.println("Thư mục cha: " + p.getParent());
System.out.println("Số thành phần: " + p.getNameCount());
Path q = Paths.get("du-lieu").resolve("../tam/./a.txt").normalize();
System.out.println("Sau normalize: " + q);
Files.createDirectories(p.getParent()); // tạo cả chuỗi thư mục cha
Files.write(p, Arrays.asList("ten,diem", "An,8"), StandardCharsets.UTF_8);
System.out.println("Tồn tại: " + Files.exists(p) + ", kích thước: " + Files.size(p));
Files.delete(p);
Files.delete(p.getParent());
Files.delete(p.getParent().getParent());
System.out.println("Đã dọn xong: " + !Files.exists(p));
}
}Khác với File.delete(), Files.delete ném NoSuchFileException nếu tệp không tồn tại hoặc DirectoryNotEmptyException nếu thư mục còn nội dung — rõ ràng hơn nhiều so với trả false. Files.createDirectories không báo lỗi nếu thư mục đã có. Dấu phân cách trong kết quả (/ hay \) phụ thuộc hệ điều hành — ví dụ chạy trên Linux của Judge0 in /.
Mẹo: Đừng ghép đường dẫn bằng cộng chuỗi
"a" + "/" + "b". DùngPaths.get("a", "b")haynew File(dir, "b")để chương trình chạy đúng trên cả Windows lẫn Linux.
Lưu ý: Đường dẫn tương đối như
"data.txt"được tính từ thư mục làm việc hiện tại của chương trình (với IntelliJ thường là thư mục gốc của dự án), không phải thư mục chứa tệp.java. GặpFileNotFoundExceptionmà tệp "rõ ràng đang ở đó" thì hãy innew File("data.txt").getAbsolutePath()ra xem.
7. Sao chép tệp
Sao chép bằng bộ đệm
Sao chép một tệp thực chất là đọc khối từ dòng vào, ghi khối ra dòng ra cho tới khi hết. Cách này dùng dòng byte nên chạy đúng với mọi loại tệp (văn bản, ảnh, tệp nén). Thủ thuật quan trọng: dùng n = in.read(buf) để biết khối cuối có bao nhiêu byte thật, và chỉ ghi n byte đó.
Files.write(Paths.get("src.bin"), new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
long total = 0;
try (InputStream in = new BufferedInputStream(new FileInputStream("src.bin"));
OutputStream out = new BufferedOutputStream(new FileOutputStream("dst.bin"))) {
byte[] buf = new byte[4]; // bộ đệm nhỏ để thấy khối cuối thiếu byte
int n;
while ((n = in.read(buf)) != -1) {
out.write(buf, 0, n); // chỉ ghi n byte thật đọc được
total += n;
System.out.println("khối " + n + " byte");
}
}
System.out.println("Đã chép " + total + " byte");
byte[] a = Files.readAllBytes(Paths.get("src.bin"));
byte[] b = Files.readAllBytes(Paths.get("dst.bin"));
System.out.println("Hai tệp giống nhau? " + Arrays.equals(a, b));Tệp 10 byte với bộ đệm 4 byte cho ba khối: 4, 4, 2. Nếu ghi out.write(buf) (cả 4 byte) thì khối cuối sẽ ghi thừa 2 byte cũ trong bộ đệm làm tệp đích sai — một lỗi rất hay gặp. Trong thực tế dùng bộ đệm 4–64 KB (ví dụ new byte[8192]).
Files.copy và các tiện ích
Khi chỉ cần chép đơn giản, Files.copy làm hộ tất cả. Kèm theo StandardCopyOption.REPLACE_EXISTING nếu cho phép ghi đè:
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
Path src = Paths.get("goc.txt");
Path dst = Paths.get("ban-sao.txt");
Files.write(src, Arrays.asList("dòng 1", "dòng 2"), StandardCharsets.UTF_8);
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Kích thước bản sao: " + Files.size(dst));
System.out.println("Nội dung: " + Files.readAllLines(dst, StandardCharsets.UTF_8));
Files.move(dst, Paths.get("doi-ten.txt"), StandardCopyOption.REPLACE_EXISTING);
System.out.println("Bản sao còn tồn tại? " + Files.exists(dst));
System.out.println("Tệp mới tồn tại? " + Files.exists(Paths.get("doi-ten.txt")));
}
}Files.move vừa di chuyển vừa đổi tên. Sao chép bằng vòng lặp thủ công vẫn đáng học vì bạn cần nó khi muốn biến đổi dữ liệu trong lúc chép (mã hóa XOR, đếm byte, tính tổng kiểm — checksum) hoặc tách một tệp thành nhiều phần.
Tách và gộp tệp
Ý tưởng tách tệp lớn thành các phần (chunk) có kích thước cố định: đọc chunkSize byte, ghi ra tệp phần tiếp theo. Gộp là việc ngược lại: lần lượt chép từng phần vào một tệp đích.
byte[] data = new byte[10];
for (int i = 0; i < data.length; i++) data[i] = (byte) (i + 1);
Files.write(Paths.get("big.bin"), data);
int chunkSize = 4, part = 0;
try (InputStream in = new FileInputStream("big.bin")) {
byte[] buf = new byte[chunkSize];
int n;
while ((n = in.readNBytes(buf, 0, chunkSize)) > 0) {
try (OutputStream out = new FileOutputStream("big.part" + part)) {
out.write(buf, 0, n);
}
System.out.println("part" + part + ": " + n + " byte");
part++;
}
}
try (OutputStream all = new FileOutputStream("big.merged")) {
for (int i = 0; i < part; i++) {
all.write(Files.readAllBytes(Paths.get("big.part" + i)));
}
}
System.out.println("Gộp lại giống bản gốc? "
+ Arrays.equals(data, Files.readAllBytes(Paths.get("big.merged"))));Điểm khác với read(buf): readNBytes(buf, 0, chunkSize) (Java 9+) đọc cho đến khi đủ chunkSize byte hoặc hết tệp, nên mỗi phần (trừ phần cuối) đúng kích thước. read(buf) có thể trả về ít hơn dù chưa hết tệp (đặc biệt với mạng), nên khi cần "đủ khối" bạn phải dùng readNBytes hoặc tự lặp.
Cảnh báo:
Files.readAllBytesnạp cả tệp vào RAM. Dùng cho phần nhỏ như trong ví dụ thì tốt, nhưng với tệp vài GB hãy chép từng khối bằng vòng lặpread/write.
Bây giờ là lúc tự tay chép, tách, gộp tệp và tính tổng kiểm:
8. Đọc dữ liệu từ stdin nhanh
Vì sao Scanner có thể chậm?
Trong các bài thi lập trình (và nhiều bài trên Codelab), đầu vào có thể lên tới hàng trăm nghìn số. Scanner rất tiện nhưng nội bộ dùng biểu thức chính quy và nhiều bước kiểm tra, nên chậm hơn BufferedReader rõ rệt ở dữ liệu lớn. Quy tắc đời thường: dữ liệu nhỏ → Scanner; dữ liệu lớn hoặc dòng-theo-dòng → BufferedReader.
| Công cụ | Ưu điểm | Nhược điểm |
|---|---|---|
Scanner |
Dễ, tự tách và đổi kiểu | Chậm với đầu vào lớn |
BufferedReader + readLine() |
Nhanh, đọc nguyên dòng | Phải tự tách (split) và đổi kiểu |
BufferedReader + StringTokenizer |
Nhanh, tách token nhẹ hơn split |
Cần quản lý token còn lại |
StreamTokenizer |
Nhanh nhất cho số | Số đọc ra là double; chữ có quy tắc riêng |
BufferedReader và split
Cách phổ biến nhất: bọc System.in bằng InputStreamReader rồi BufferedReader. readLine() trả về cả dòng hoặc null khi hết dữ liệu (EOF). Muốn lấy từng số trên dòng thì split theo khoảng trắng rồi Integer.parseInt:
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
String[] parts = br.readLine().trim().split("\\s+");
long sum = 0;
for (int i = 0; i < n; i++) sum += Long.parseLong(parts[i]);
System.out.println("Tổng = " + sum);Hai chi tiết nhỏ nhưng quan trọng: trim() loại khoảng trắng thừa hai đầu, và split("\\s+") tách theo một hay nhiều khoảng trắng (nếu chỉ split(" ") mà đầu vào có hai dấu cách liền nhau, bạn sẽ có phần tử rỗng và parseInt("") ném NumberFormatException).
Đọc đến hết dữ liệu (EOF)
Nhiều bài không cho biết trước số dòng; bạn đọc đến khi readLine() trả null. Ví dụ đếm số dòng và tổng số từ, dùng StringTokenizer để tách từ:
import java.nio.charset.StandardCharsets;
public class Main {
public static void main(String[] args) throws Exception {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
String line;
int lines = 0, words = 0, empty = 0;
while ((line = br.readLine()) != null) {
lines++;
if (line.trim().isEmpty()) { empty++; continue; }
words += new StringTokenizer(line).countTokens();
}
System.out.println("Số dòng: " + lines + ", dòng trống: " + empty + ", số từ: " + words);
}
}StringTokenizer mặc định tách theo khoảng trắng, tab, xuống dòng; countTokens() cho số token còn lại. Một dòng stdin trống (// @stdin không giá trị) cho readLine() trả về chuỗi rỗng "", khác với null — dòng trống vẫn là một dòng.
StreamTokenizer: đọc số nhanh
StreamTokenizer tự cắt đầu vào thành token, nhận ra số ngay trong lúc đọc và để kết quả trong trường nval (kiểu double), còn chữ trong sval. Với đầu vào chỉ toàn số nguyên, đây là cách nhanh nhất:
StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
st.nextToken();
int n = (int) st.nval;
long sum = 0;
int max = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
st.nextToken();
int v = (int) st.nval;
sum += v;
max = Math.max(max, v);
}
System.out.println("n = " + n + ", tổng = " + sum + ", lớn nhất = " + max);Số trải trên nhiều dòng cũng được đọc đúng vì StreamTokenizer không quan tâm ranh giới dòng. Hạn chế: nval là double nên số nguyên rất lớn (trên 2 mũ 53) sẽ mất chính xác; từ có ký tự đặc biệt/tiếng Việt cần cấu hình thêm. Với bài toán chữ hoặc dòng lẫn lộn, hãy dùng BufferedReader + StringTokenizer.
Gom output rồi in một lần
System.out.println mỗi lần gọi có thể "xả" (flush) ra màn hình, mỗi lần tốn thời gian. Khi cần in hàng chục nghìn dòng, hãy gom vào StringBuilder rồi in một lần (hoặc dùng PrintWriter/BufferedWriter):
int n = new Scanner(System.in).nextInt();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= 10; i++) {
sb.append(n).append(" x ").append(i).append(" = ").append(n * i).append('\n');
}
System.out.print(sb);Với số thực và canh cột, dùng printf/String.format. Hãy nhớ truyền Locale.US nếu cần dấu chấm thập phân cố định, vì bản địa hóa mặc định có thể in dấu phẩy:
String[] names = {"An", "Binh", "Chi"};
double[] gpas = {8.5, 7.25, 9.0};
for (int i = 0; i < names.length; i++) {
System.out.print(String.format(Locale.US, "%-8s|%6.2f%n", names[i], gpas[i]));
}%-8s canh trái trong 8 ký tự, %6.2f canh phải rộng 6 với 2 chữ số thập phân, %n là xuống dòng đúng hệ điều hành.
Mẹo: Ghi nhớ khuôn đọc stdin nhanh: tạo một
BufferedReader brở đầumain, dùngbr.readLine()cho dòng,new StringTokenizer(br.readLine())cho từng dòng số; kết thúc bằng mộtStringBuilderin một lần.
Cảnh báo: Không trộn
ScannervàBufferedReadertrên cùngSystem.in: mỗi lớp có bộ đệm riêng và sẽ "ăn mất" dữ liệu của lớp kia. Chọn một cách cho cả chương trình.
Để kết thúc buổi, hãy luyện đọc nhanh stdin và xử lý dữ liệu có cấu trúc như CSV, nhật ký truy cập.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Quên đóng dòng ghi (close) |
Dữ liệu kẹt trong bộ đệm, tệp rỗng hoặc cụt | Dùng try-with-resources |
Dùng FileReader/FileWriter mặc định cho tiếng Việt |
Chữ thành ? hoặc vỡ khi đổi máy |
InputStreamReader/OutputStreamWriter hoặc Files kèm UTF_8 |
| Đọc dữ liệu nhị phân khác thứ tự/kiểu lúc ghi | Giá trị rác hoặc EOFException |
Ghi/đọc cùng thứ tự và cùng kiểu; ghi số lượng trước |
Chép tệp bằng out.write(buf) thay vì write(buf, 0, n) |
Tệp đích thừa byte ở khối cuối | Luôn ghi đúng n byte vừa đọc |
Quên newLine() sau readLine() khi ghi lại |
Các dòng dính vào nhau | out.write(line); out.newLine(); |
split(" ") với khoảng trắng thừa |
Phần tử rỗng, NumberFormatException |
trim() rồi split("\\s+") |
Trộn Scanner và BufferedReader trên System.in |
Mất dữ liệu đầu vào | Chọn một công cụ cho cả chương trình |
Quên Serializable hoặc dùng transient sai |
NotSerializableException, trường bị mất |
Cài đặt giao diện; chỉ transient cho dữ liệu không cần lưu |
Đọc File tương đối nhưng sai thư mục làm việc |
FileNotFoundException |
In getAbsolutePath() kiểm tra |
Tóm tắt
| Chủ đề | Ghi nhớ |
|---|---|
| Dòng | Byte (InputStream/OutputStream) cho nhị phân; ký tự (Reader/Writer) cho văn bản |
| Decorator | Bọc dòng: tệp → Buffered → Data/Object |
| try-with-resources | Tự đóng, theo thứ tự ngược với thứ tự mở |
| Nhị phân | Data…Stream: ghi/đọc cùng thứ tự và cùng kiểu; EOFException khi hết |
| Văn bản | BufferedReader.readLine() (hết là null), BufferedWriter.newLine(), luôn chỉ rõ UTF-8 |
| Đối tượng | Serializable, writeObject/readObject, transient, serialVersionUID |
| Tệp/thư mục | File (cũ), Path/Paths/Files (mới); Files.copy/move/delete |
| Sao chép | Đọc khối, ghi đúng n byte; tách/gộp bằng readNBytes |
| stdin nhanh | BufferedReader + StringTokenizer/split, StreamTokenizer; gom output bằng StringBuilder |