1. Giới thiệu
Tuần tự hóa tự động — giống như chế độ lái tự động trên máy bay: hoạt động rất tốt chừng nào mọi thứ đi đúng kế hoạch. Nhưng khi xuất hiện các điều kiện đặc biệt, sẽ thấy cơ chế đơn giản đó không còn đủ. Hãy tưởng tượng bạn cần lưu một đối tượng nhưng không phải mọi trường của nó: có dữ liệu là tạm thời, có dữ liệu quá nhạy cảm để ghi ra tệp. Hoặc ngược lại — khi lưu cần thêm thứ gì đó của riêng bạn: chẳng hạn phiên bản hoặc kiểm tra tổng. Cũng có khi trước khi ghi hay tải dữ liệu, bạn cần thực hiện kiểm tra hoặc chuyển đổi. Và đôi khi nhiệm vụ còn khó hơn: đảm bảo tương thích với các phiên bản trước của lớp nếu cấu trúc của nó thay đổi theo thời gian.
Trong những tình huống như vậy, rõ ràng tuần tự hóa tiêu chuẩn là không đủ. Cần nắm quyền kiểm soát trong tay bạn.
Các phương thức đặc biệt của tuần tự hóa: writeObject và readObject
Trong Java có hai phương thức đặc biệt cho phép bạn kiểm soát hoàn toàn quá trình tuần tự hóa và giải tuần tự của đối tượng:
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Quan trọng!
- Các phương thức phải đúng là private (không phải public, không phải protected, cũng không phải package-private).
- Chữ ký phải khớp chính xác với những gì nêu ở trên.
- Nếu các phương thức này được khai báo trong lớp của bạn, chúng sẽ được gọi thay cho tuần tự hóa/giải tuần tự tiêu chuẩn.
Cơ chế hoạt động như thế nào?
Khi bạn gọi ObjectOutputStream.writeObject(obj), JVM trước hết sẽ tìm trong lớp của obj phương thức private void writeObject(ObjectOutputStream). Nếu có — chính phương thức đó sẽ được gọi. Tương tự, khi giải tuần tự sẽ gọi private void readObject(ObjectInputStream).
Nếu các phương thức không được khai báo, sẽ dùng tuần tự hóa tiêu chuẩn.
Cấu trúc của writeObject và readObject
Chữ ký phương thức
private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
Bên trong các phương thức này, bạn bắt buộc phải gọi:
- out.defaultWriteObject(); — để tuần tự hóa các trường chuẩn (không transient) của siêu lớp và lớp hiện tại.
- in.defaultReadObject(); — để giải tuần tự các trường chuẩn.
Nếu bạn không gọi các phương thức này, các trường chuẩn sẽ không được tuần tự hóa — và khi giải tuần tự, đối tượng sẽ trở nên “trống rỗng”. Giống như quên bỏ hộ chiếu vào va-li: về hình thức bạn đã đến nơi, nhưng sẽ không thể chứng minh bạn là ai.
2. Ví dụ: thêm kiểm tra tổng (checksum) khi tuần tự hóa
Hãy xem một ví dụ thực tế. Giả sử chúng ta có một lớp người dùng và muốn khi tuần tự hóa thêm vào đối tượng một kiểm tra tổng để khi giải tuần tự kiểm tra tính toàn vẹn dữ liệu.
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// Trường transient — không tuần tự hóa nó
private transient int checksum;
public User(String name, int age) {
this.name = name;
this.age = age;
this.checksum = calculateChecksum();
}
private int calculateChecksum() {
return (name != null ? name.hashCode() : 0) + age;
}
// Tuần tự hóa tùy biến
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // Lưu các trường chuẩn
int sum = calculateChecksum();
out.writeInt(sum); // Ghi kiểm tra tổng
System.out.println("[LOG] Tuần tự hóa User: checksum=" + sum);
}
// Giải tuần tự tùy biến
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // Khôi phục các trường chuẩn
int sum = in.readInt(); // Đọc kiểm tra tổng
int actual = calculateChecksum();
System.out.println("[LOG] Giải tuần tự User: checksum=" + sum + ", actual=" + actual);
if (sum != actual) {
throw new IOException("Dữ liệu bị hỏng! Kiểm tra tổng không khớp.");
}
this.checksum = actual;
}
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
}
}
Ví dụ sử dụng:
// Lưu đối tượng
User user = new User("Alice", 42);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(user);
}
// Tải đối tượng
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User loaded = (User) in.readObject();
System.out.println("Đối tượng đã khôi phục: " + loaded);
}
Chuyện gì xảy ra?
- Khi tuần tự hóa, writeObject được gọi, các trường chuẩn + kiểm tra tổng được lưu.
- Khi giải tuần tự, readObject được gọi, các trường được khôi phục + kiểm tra kiểm tra tổng.
- Trong console sẽ có log; nếu có gì sai — sẽ ném ra ngoại lệ.
3. Loại trừ dữ liệu nhạy cảm khỏi tuần tự hóa
Đôi khi cần một số trường không được tuần tự hóa (ví dụ, mật khẩu). Có thể dùng từ khóa transient (sẽ nói chi tiết hơn ở bài sau), nhưng bạn cũng có thể không tuần tự hóa trường đó bằng tay nếu bạn triển khai writeObject.
Ví dụ:
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient — không được tuần tự hóa
// Nhưng cũng có thể làm như sau:
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// Không ghi password!
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// password vẫn là null
}
}
Lưu ý:
Nếu bạn muốn chỉ tuần tự hóa một phần đối tượng — chỉ cần đừng ghi các trường thừa vào luồng.
4. Gọi các phương thức của siêu lớp: defaultWriteObject và defaultReadObject
Bên trong các phương thức writeObject và readObject của bạn, hầu như luôn cần gọi defaultWriteObject() và defaultReadObject(). Điều này giống như bấm “lưu bản nháp” trước khi bạn thêm ghi chú của riêng mình.
Các phương thức này chịu trách nhiệm cho tuần tự hóa tiêu chuẩn mọi trường không-transient, không-static của lớp hiện tại và siêu lớp. Nếu không gọi chúng, các trường này sẽ không được tuần tự hóa và khi giải tuần tự sẽ rỗng.
Ví dụ hành vi sai:
private void writeObject(ObjectOutputStream out) throws IOException {
// out.defaultWriteObject(); // quên gọi!
out.writeInt(123); // ghi thêm của riêng bạn
}
Trong trường hợp này, các trường chuẩn sẽ không được lưu!
5. Thực hành: ghi log quá trình tuần tự hóa
Hãy thêm ghi log vào lớp người dùng của chúng ta để thấy khi nào diễn ra tuần tự hóa và giải tuần tự.
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
private void writeObject(ObjectOutputStream out) throws IOException {
System.out.println("[LOG] Tuần tự hóa Person: " + name + ", tuổi " + age);
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
System.out.println("[LOG] Giải tuần tự Person: " + name + ", tuổi " + age);
}
}
Sử dụng:
Person p = new Person("Bob", 30);
// Lưu vào tệp
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
out.writeObject(p);
}
// Tải từ tệp
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
Person loaded = (Person) in.readObject();
}
Kết quả:
Trong console bạn sẽ thấy các thông báo về việc đối tượng được tuần tự hóa và giải tuần tự.
6. Những lỗi thường gặp khi dùng writeObject/readObject
Lỗi №1: Không gọi defaultWriteObject/defaultReadObject. Nếu quên gọi các phương thức này, các trường chuẩn sẽ không được tuần tự hóa và đối tượng sau khi giải tuần tự sẽ rỗng hoặc không chính xác.
Lỗi №2: Chữ ký phương thức không đúng. Các phương thức phải chính xác là private void writeObject(ObjectOutputStream) và private void readObject(ObjectInputStream). Nếu đặt chúng là public/protected hoặc thay đổi tham số — chúng sẽ không được gọi tự động.
Lỗi №3: Ngoại lệ trong phương thức. Nếu trong writeObject hoặc readObject xảy ra ngoại lệ, việc tuần tự hóa hoặc giải tuần tự sẽ bị dừng và đối tượng sẽ không được lưu/tải một cách chính xác.
Lỗi №4: Quên tuần tự hóa/giải tuần tự của siêu lớp. Nếu lớp của bạn kế thừa từ một lớp có thể tuần tự hóa, nhất định phải gọi defaultWriteObject/defaultReadObject, nếu không các trường của siêu lớp sẽ không được lưu.
Lỗi №5: Tuần tự hóa dữ liệu nhạy cảm. Nếu bạn quên loại trừ mật khẩu hoặc dữ liệu riêng tư khác, chúng sẽ vào tệp đã tuần tự hóa. Hãy dùng transient hoặc không tuần tự hóa chúng bằng tay.
GO TO FULL VERSION