CodeGym /Các khóa học /JAVA 25 SELF /Externalizable: tinh chỉnh chi tiết việc tuần tự hóa

Externalizable: tinh chỉnh chi tiết việc tuần tự hóa

JAVA 25 SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Trong Java, để tuần tự hóa đối tượng, người ta thường dùng giao diện Serializable. Nó đơn giản: chỉ cần triển khai giao diện là có thể ghi/đọc đối tượng bằng ObjectOutputStream/ObjectInputStream. Tuy nhiên đôi khi như vậy là chưa đủ:

  • Cần kiểm soát hoàn toàn xem những trường nào được tuần tự hóa và theo cách nào.
  • Cần đảm bảo khả năng tương thích giữa các phiên bản khác nhau của lớp.
  • Quan trọng là giảm kích thước tệp đã tuần tự hóa hoặc tăng tốc độ xử lý.

Cho những trường hợp như vậy, Java có giao diện Externalizable — một cách tuần tự hóa “thủ công” và linh hoạt hơn.

Tóm tắt:

  • Serializable — tuần tự hóa tự động: Java tự quyết định ghi/đọc cái gì và như thế nào.
  • Externalizable — tuần tự hóa thủ công: bạn tự xác định những gì cần lưu/khôi phục và cách thực hiện.

2. Hợp đồng Externalizable: triển khai writeExternalreadExternal

Để sử dụng Externalizable, cần:

  1. Triển khai giao diện java.io.Externalizable.
  2. Bắt buộc triển khai hai phương thức:
    • void writeExternal(ObjectOutput out) throws IOException
    • void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

Ví dụ:

import java.io.*;

public class User implements Externalizable {
    private String name;
    private int age;

    // Bắt buộc phải có constructor public không tham số!
    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Quan trọng: lập trình viên tự quyết định các trường nào sẽ được tuần tự hóa và theo thứ tự nào. Tuy nhiên có một yêu cầu bắt buộc: lớp phải có constructor public không tham số. Nếu không có, khi giải tuần tự chương trình sẽ ném InvalidClassException.

3. Khi nào nên dùng Externalizable?

Hãy dùng Externalizable khi:

  • Cần kiểm soát hoàn toàn định dạng dữ liệu. Ví dụ, bạn chỉ muốn tuần tự hóa một phần trường, hoặc tuần tự hóa theo thứ tự/định dạng đặc biệt.
  • Tối ưu hiệu năng và kích thước tệp. Tuần tự hóa chuẩn bổ sung thông tin phục vụ (metadata, tên lớp, kiểu, v.v.). Với Externalizable, bạn chỉ ghi dữ liệu cần thiết.
  • Đảm bảo tương thích ngược. Nếu cấu trúc lớp thay đổi, bạn có thể tự viết logic đọc dữ liệu của các phiên bản cũ và mới.
  • Tuần tự hóa các đối tượng không chuẩn. Ví dụ, khi có các trường không thể tuần tự hóa theo cách chuẩn (chẳng hạn transient, volatile, hoặc cấu trúc phức tạp).

Khi nào KHÔNG nên dùng?

  • Nếu bạn không cần kiểm soát hoàn toàn — hãy dùng Serializable, nó đơn giản và an toàn hơn.
  • Nếu bạn không chắc có thể duy trì tương thích định dạng dữ liệu khi lớp thay đổi.

4. Ưu và nhược điểm của Externalizable so với Serializable

Ưu điểm:

  • Kiểm soát hoàn toàn việc tuần tự hóa. Bạn tự quyết định ghi/đọc cái gì và như thế nào.
  • Gọn nhẹ. Không có metadata thừa — chỉ có dữ liệu của bạn.
  • Tốc độ. Ít dữ liệu hơn — ghi/đọc nhanh hơn.
  • Linh hoạt. Có thể hỗ trợ nhiều phiên bản định dạng, thêm nén, mã hóa, v.v.

Nhược điểm:

  • Triển khai thủ công — dễ mắc lỗi. Nếu lẫn lộn thứ tự ghi/đọc, việc tuần tự hóa sẽ “hỏng” (gây lỗi hoặc dữ liệu không chính xác).
  • Không có hỗ trợ tự động cho transient, serialVersionUID. Mọi thứ bạn phải tự thiết kế và triển khai.
  • Khó bảo trì hơn. Khi cấu trúc lớp thay đổi, cần nhớ cập nhật các phương thức tuần tự hóa.
  • Bắt buộc phải có constructor public không tham số.
  • Ít “ma thuật” hơn — nhiều trách nhiệm hơn.

5. Ví dụ: tuần tự hóa và giải tuần tự một đối tượng đơn giản

Tuần tự hóa đối tượng

User user = new User("Alice", 30);

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

Giải tuần tự đối tượng

try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println(loaded); // Alice (30)
}

Lưu ý: nếu bạn thay đổi thứ tự ghi/đọc trường, hoặc quên tuần tự hóa một trường nào đó, dữ liệu sẽ bị sai! Hai phương thức writeExternalreadExternal phải khớp chặt chẽ về trình tự thao tác.

Ví dụ: chỉ tuần tự hóa một phần trường

public class SecretUser implements Externalizable {
    private String login;
    private transient String password; // transient không có ý nghĩa đối với Externalizable

    public SecretUser() {}

    public SecretUser(String login, String password) {
        this.login = login;
        this.password = password;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(login);
        // Không tuần tự hóa mật khẩu!
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        login = in.readUTF();
        password = null; // không khôi phục mật khẩu
    }
}

6. Thực hành: so sánh kích thước tệp đã tuần tự hóa

Hãy so sánh các tệp được tuần tự hóa qua Serializable và qua Externalizable “nặng” bao nhiêu.

Lớp dùng Serializable

public class UserSerializable implements Serializable {
    private String name;
    private int age;

    public UserSerializable(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Lớp dùng Externalizable

public class UserExternalizable implements Externalizable {
    private String name;
    private int age;

    public UserExternalizable() {}

    public UserExternalizable(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }
}

Mã để so sánh

import java.io.*;

public class CompareSerialization {
    public static void main(String[] args) throws Exception {
        UserSerializable s = new UserSerializable("Bob", 25);
        UserExternalizable e = new UserExternalizable("Bob", 25);

        // Serializable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
            out.writeObject(s);
        }

        // Externalizable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
            out.writeObject(e);
        }

        System.out.println("Serializable file size: " + new File("ser.bin").length());
        System.out.println("Externalizable file size: " + new File("ext.bin").length());
    }
}

Kết quả:
Tệp ser.bin (Serializable) thường lớn hơn — chứa thông tin phục vụ của Java. Tệp ext.bin (Externalizable) — chỉ gồm dữ liệu của bạn, thường nhỏ hơn.

7. Các lỗi thường gặp khi làm việc với Externalizable

Lỗi 1: Thiếu constructor public không tham số.
Lớp triển khai Externalizable bắt buộc phải có constructor public không đối số. Nếu thiếu, giải tuần tự sẽ gây ra InvalidClassException.

Lỗi 2: Sai thứ tự ghi và đọc các trường.
Hai phương thức writeExternalreadExternal phải hoạt động theo cùng một thứ tự. Nếu khi ghi lưu trường name trước, nhưng khi đọc lại cố gắng đọc age trước, dữ liệu sẽ bị sai lệch.

Lỗi 3: Bỏ sót trường khi tuần tự hóa.
Nếu quên ghi một trường trong writeExternal, khi giải tuần tự trường đó sẽ có giá trị null (đối với kiểu tham chiếu) hoặc 0 (đối với kiểu số).

Lỗi 4: Sử dụng transient hoặc serialVersionUID không đúng.
Không giống Serializable, với Externalizable các cơ chế này không tự động hoạt động — bạn phải tự kiểm soát trường nào lưu, trường nào không.

Lỗi 5: Thay đổi cấu trúc lớp mà không cập nhật các phương thức.
Nếu thêm hoặc xóa trường nhưng không cập nhật tương ứng trong writeExternalreadExternal, dữ liệu đã lưu trước đó có thể không còn được nạp chính xác.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION