Cơ chế tuần tự hóa và khôi phục đối tượng trong Java qua JDK Serialization

Trong lập trình Java, tuần tự hóa (serialization) là quá trình chuyển đổi trạng thái của một đối tượng thành chuỗi byte để lưu trữ hoặc truyền tải. Ngược lại, khôi phục (deserialization) là quá trình tái tạo lại đối tượng từ chuỗi byte đó.

Lý do cần tuần tự hóa

Đối tượng Java chỉ tồn tại trong bộ nhớ heap của JVM, không thể trực tiếp chia sẻ giữa các tiến trình hay gửi qua mạng. Tuần tự hóa giúp biến đối tượng thành dữ liệu nhị phân có thể:

  • Gửi giữa các tiến trình (RPC, RMI)
  • Truyền qua mạng
  • Lưu trữ lâu dài vào file hoặc database

Cách triển khai với JDK Serialization

Cách đơn giản nhất là cho lớp cần tuần tự hóa implements interface Serializable. Đây là marker interface – không chứa phương thức nào, chỉ dùng để đánh dấu.

Ví dụ minh họa:

class AddressInfo implements Serializable {
    private String location;

    // getter/setter
}

class Institution {
    private String name;
    // getter/setter — không implements Serializable → sẽ gây lỗi nếu được tham chiếu
}

class BaseEntity implements Serializable {
    protected String category;
    // getter/setter
}

class Member extends BaseEntity implements Serializable {
    private static boolean isActive;        // static → không được serialize
    private Long id;
    private int years;
    private String loginName;
    private transient String ssn;           // transient → bị bỏ qua
    private Date registeredAt;
    private AddressInfo residence;          // OK vì implements Serializable
    private Institution organization;       // LỖI nếu gán giá trị
    private List<Member> connections;       // OK nếu các phần tử đều serializable

    // getter/setter
}

Khi chạy thử, nếu cố gắng serialize trường organization (không serializable), chương trình sẽ ném NotSerializableException. Tương tự, nếu lớp cha BaseEntity không implements Serializable, trường category sẽ bị mất sau khi deserialize.

Nguyên lý hoạt động bên trong

Quá trình thực hiện bởi ObjectOutputStreamObjectInputStream.

Serialize:

  • writeObject() → gọi writeObject0()
  • Xác định kiểu dữ liệu: String, mảng, enum → xử lý riêng
  • Đối tượng thông thường → gọi writeOrdinaryObject()
  • Kiểm tra xem lớp có ghi đè writeObject() không → nếu có thì invoke qua reflection
  • Nếu không → gọi defaultWriteFields() để ghi từng field
  • Field là object → đệ quy tiếp

Deserialize:

  • readObject() → gọi readObject0()
  • Đọc byte đầu tiên để xác định kiểu dữ liệu
  • Với object → gọi readOrdinaryObject()
  • Dùng reflection tạo instance qua constructor không tham số
  • Kiểm tra xem có ghi đè readObject() không → nếu có thì invoke
  • Nếu không → gọi defaultReadFields() để gán giá trị từng field

Tuỳ chỉnh quá trình serialize/deserialize

Có thể ghi đè hai phương thức private sau để kiểm soát chi tiết:

private void writeObject(ObjectOutputStream out) throws IOException {
    out.writeLong(this.id);
    out.writeObject(this.loginName);
    out.writeObject(this.ssn); // dù là transient nhưng vẫn ghi thủ công
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    this.id = in.readLong();
    this.loginName = (String) in.readObject();
    this.ssn = (String) in.readObject();
}

Lưu ý:

  • Phải ghi đè cả hai phương thức
  • Thứ tự đọc/ghi phải khớp nhau
  • Phải là private — modifier khác sẽ không có hiệu lực

Dùng Externalizable để kiểm soát toàn bộ

Thay vì Serializable, có thể implements Externalizable và bắt buộc override:

public void writeExternal(ObjectOutput out) throws IOException {
    out.writeLong(id);
    out.writeObject(loginName);
}

public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    this.id = in.readLong();
    this.loginName = (String) in.readObject();
}

Xử lý vấn đề với Singleton

Tuần tự hóa có thể phá vỡ mẫu Singleton vì sẽ tạo instance mới khi deserialize. Giải pháp: thêm phương thức readResolve():

public class UniqueInstance implements Serializable {
    private static final UniqueInstance INSTANCE = new UniqueInstance();

    public static UniqueInstance getInstance() { return INSTANCE; }

    private Object readResolve() {
        return INSTANCE; // luôn trả về instance duy nhất
    }
}

Phương thức này phải trả về Object — nếu trả về kiểu cụ thể (UniqueInstance) thì sẽ không được gọi.

Vai trò của serialVersionUID

Là ID phiên bản lớp, dùng để kiểm tra tính tương thích khi deserialize. Nếu không khai báo, JVM sẽ tự sinh — nhưng dễ gây lỗi nếu class thay đổi. Nên khai báo tường minh:

private static final long serialVersionUID = 1L;

Nếu version không khớp, sẽ ném InvalidClassException.

So sánh với các cơ chế khác

Cơ chếKích thướcTốc độƯu điểm
JDKLớnChậmKhông cần thư viện ngoài, đáng tin cậy
HessianTrung bìnhKhá nhanhHỗ trợ đa ngôn ngữ, tối ưu tốt hơn JDK
JSONLớnKhá nhanhDễ đọc, hỗ trợ đa ngôn ngữ
KryoRất nhỏRất nhanhHiệu năng cao, nhưng chỉ dùng trong Java
ProtoBufRất nhỏRất nhanhHiệu năng cao, hỗ trợ đa ngôn ngữ, cần định nghĩa schema trước

JDK Serialization phù hợp cho các ứng dụng nội bộ Java cần độ tin cậy cao, trong khi các giải pháp khác tối ưu hơn cho hiệu năng và kích thước.

Thẻ: Java Serialization jdk objectinputstream objectoutputstream

Đăng vào ngày 11 tháng 6 lúc 03:22