CodeGym /Các khóa học /JAVA 25 SELF /Tùy chỉnh hành vi tuần tự hóa: các phương thức tùy chỉnh

Tùy chỉnh hành vi tuần tự hóa: các phương thức tùy chỉnh

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

1. Các phương thức writeReplace và readResolve: lý thuyết

Đôi khi các cơ chế tuần tự hóa mặc định là chưa đủ. Hãy hình dung: bạn có một singleton (lớp chỉ có thể có một thể hiện trong toàn bộ chương trình), và bạn muốn sau khi giải tuần tự hóa, nó vẫn là thể hiện duy nhất (không xuất hiện một bản sao mới). Hoặc bạn muốn tuần tự hóa không phải chính đối tượng, mà là phiên bản “nhẹ” của nó (proxy) để ẩn chi tiết hiện thực hoặc tiết kiệm dung lượng.

Trong Java có các phương thức đặc biệt cho việc này: writeReplacereadResolve. Nhiệm vụ của chúng là thay thế đối tượng đang được tuần tự hóa hoặc giải tuần tự hóa bằng một đối tượng khác.

Ví dụ minh họa đơn giản:
Giống như bạn gửi một bưu kiện cho bạn mình, nhưng thay vì chính bạn, bạn đặt một “bản sao đồ chơi” vào hộp. Khi người bạn mở hộp, thay vì món đồ chơi, trên tay anh ấy lại là bạn — phiên bản thật! (Ngoài đời không thế được, nhưng trong Java — hoàn toàn có thể.)

writeReplace

Phương thức private Object writeReplace() được gọi trên đối tượng trước khi tuần tự hóa. Nó có thể trả về bất kỳ đối tượng nào, và đối tượng đó sẽ thực sự được tuần tự hóa thay cho đối tượng gốc. Nếu không triển khai — chính đối tượng gốc sẽ được tuần tự hóa.

Chữ ký phương thức:


private Object writeReplace() throws ObjectStreamException

readResolve

Phương thức private Object readResolve() được gọi trên đối tượng sau khi giải tuần tự hóa. Nó cho phép thay thế đối tượng vừa được tạo bằng một đối tượng khác (ví dụ trả về singleton hoặc thể hiện đã được cache).

Chữ ký phương thức:

private Object readResolve() throws ObjectStreamException

Quan trọng:
Cả hai phương thức phải là private và trả về Object. Đây là yêu cầu của đặc tả tuần tự hóa Java. Nếu đặt chúng là public — cơ chế tuần tự hóa sẽ bỏ qua chúng.

2. Ứng dụng writeReplace và readResolve trong thực tế

Singleton và readResolve

Singleton đơn giản là một lớp mà trong toàn bộ chương trình chỉ có thể có một thể hiện. Nếu tuần tự hóa và sau đó khôi phục đối tượng như vậy, thì nếu không có readResolve, sẽ xuất hiện một thể hiện mới và quy tắc “duy nhất” bị vi phạm. Với readResolve, bạn có thể trả về chính đối tượng duy nhất đó, giữ vững ý tưởng singleton.

import java.io.*;

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

    public static MySingleton getInstance() {
        return INSTANCE;
    }

    // Đảm bảo rằng sau khi giải tuần tự sẽ trả về đúng INSTANCE
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}

Giải thích:
Nếu không có readResolve, sau khi giải tuần tự hóa sẽ xuất hiện một đối tượng mới, không cùng tham chiếu (theo ==) với singleton gốc. Với readResolve — luôn trả về INSTANCE.

Kiểm tra thực tế:

MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();

ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();

System.out.println(s1 == s2); // true nếu có readResolve; false — nếu không có

writeReplace: tuần tự hóa đối tượng proxy

Đôi khi đối tượng quá “nặng” để tuần tự hóa, hoặc chứa dữ liệu nhạy cảm, hoặc đơn giản là không nên để lộ ra đầy đủ. Khi đó, ta có thể tuần tự hóa “vật thay thế” — một đối tượng proxy.

Ví dụ:
Giả sử chúng ta có lớp User với mật khẩu riêng tư. Ta không muốn mật khẩu bị tuần tự hóa.

import java.io.*;

public class User implements Serializable {
    private String username;
    private transient String password; // transient — không được tuần tự hóa

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // Thay vì User, chỉ tuần tự hóa UserProxy
    private Object writeReplace() throws ObjectStreamException {
        return new UserProxy(username);
    }

    // Lớp proxy — chỉ dùng cho tuần tự hóa
    private static class UserProxy implements Serializable {
        private String username;
        public UserProxy(String username) {
            this.username = username;
        }

        private Object readResolve() throws ObjectStreamException {
            // Trong thực tế không thể khôi phục mật khẩu — trả về User với mật khẩu rỗng
            return new User(username, "");
        }
    }
}

Giải thích:

  • Khi tuần tự hóa, User biến thành UserProxy (không có mật khẩu).
  • Khi giải tuần tự hóa, UserProxy biến ngược lại thành User (nhưng mật khẩu trống).

3. Tùy biến tuần tự hóa cho đối tượng bất biến

Các đối tượng bất biến thường dùng các trường final riêng tư và không có setter. Với tuần tự hóa mặc định, Java có thể vượt qua hạn chế này, nhưng đôi khi tốt hơn là kiểm soát tường minh quy trình qua writeReplace/readResolve.

Ví dụ: Value Object

import java.io.*;

public final class Money implements Serializable {
    private final int amount;
    private final String currency;

    public Money(int amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }

    private Object writeReplace() throws ObjectStreamException {
        return new MoneyProxy(amount, currency);
    }

    private static class MoneyProxy implements Serializable {
        private final int amount;
        private final String currency;

        MoneyProxy(int amount, String currency) {
            this.amount = amount;
            this.currency = currency;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Money(amount, currency);
        }
    }
}

Giải thích:

  • Khi tuần tự hóa, Money biến thành MoneyProxy (POJO).
  • Khi giải tuần tự hóa, MoneyProxy biến ngược lại thành Money.

Tương tác với writeObject/readObject

Các phương thức writeReplace/readResolve hoạt động độc lập với writeObject/readObject. Nếu cả hai cơ chế đều được định nghĩa, thì trước tiên gọi writeReplace, và với đối tượng được trả về, mới gọi writeObject (nếu nó triển khai Serializable).

Sơ đồ:

flowchart LR
    A[Đối tượng] -- writeReplace --> B[Đối tượng proxy]
    B -- writeObject --> C[Luồng byte]
    C -- readObject --> D[Đối tượng proxy]
    D -- readResolve --> E[Đối tượng kết quả]

4. Thực hành: tuần tự hóa với việc thay thế đối tượng

Hãy thêm cơ chế tuần tự hóa tùy chỉnh vào ứng dụng học tập của bạn — chẳng hạn, cho lớp Person, để khi tuần tự hóa chỉ ghi tên, còn tuổi thì bỏ qua (giả sử chúng ta quan tâm đến quyền riêng tư).

Bước 1. Lớp chính

import java.io.*;

public class Person implements Serializable {
    private String name;
    private int age; // không muốn tuần tự hóa

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

    private Object writeReplace() throws ObjectStreamException {
        return new PersonProxy(name);
    }

    private static class PersonProxy implements Serializable {
        private final String name;

        PersonProxy(String name) {
            this.name = name;
        }

        private Object readResolve() throws ObjectStreamException {
            return new Person(name, -1); // -1 — "tuổi không xác định"
        }
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

Bước 2. Kiểm thử

public class TestCustomSerialization {
    public static void main(String[] args) throws Exception {
        Person original = new Person("Alice", 30);

        // Tuần tự hóa
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
        out.writeObject(original);
        out.close();

        // Giải tuần tự hóa
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
        Person deserialized = (Person) in.readObject();
        in.close();

        System.out.println("Trước khi tuần tự hóa: " + original);
        System.out.println("Sau khi giải tuần tự hóa: " + deserialized);
    }
}

Kết quả:

Trước khi tuần tự hóa: Person{name='Alice', age=30}
Sau khi giải tuần tự hóa: Person{name='Alice', age=-1}

Như bạn thấy, tuổi không được tuần tự hóa — mọi thứ đúng kế hoạch!

5. Đặc điểm và lưu ý

Khi nào nên dùng writeReplace/readResolve?

  • Khi cần chỉ tuần tự hóa một phần trạng thái của đối tượng.
  • Để tuần tự hóa/giải tuần tự hóa các đối tượng proxy.
  • Để hỗ trợ mẫu thiết kế Singleton.
  • Cho các đối tượng bất biến hoặc phức tạp có cấu trúc bên trong có thể thay đổi.

Khi nào không nên dùng?

  • Nếu có thể dùng các trường transient hoặc writeObject/readObject.
  • Nếu đối tượng không nên bị thay thế bằng một đối tượng khác.

Tương thích với kế thừa

Nếu lớp cha định nghĩa writeReplace/readResolve, chúng cũng sẽ được gọi cho các lớp con (nếu không bị ghi đè). Hãy cẩn trọng với các hệ phân cấp lớp!

6. Những lỗi thường gặp khi tùy chỉnh tuần tự hóa

Lỗi số 1: Phạm vi truy cập không đúng. Nếu đặt writeReplace/readResolve không phải là private, cơ chế tuần tự hóa sẽ không gọi chúng. Chỉ private!

Lỗi số 2: Sai kiểu trả về. writeReplace/readResolve phải trả về Object. Dù thực tế bạn trả về kiểu của mình — vẫn phải khai báo kiểu trả về là Object.

Lỗi số 3: Mất dữ liệu. Nếu đối tượng proxy không chứa đủ dữ liệu cần thiết để khôi phục đối tượng gốc, một phần thông tin sẽ bị mất. Luôn đảm bảo bạn có thể tái tạo lại đối tượng.

Lỗi số 4: Vi phạm bất biến. readResolve phải trả về đối tượng phù hợp với kỳ vọng của chương trình (ví dụ, với singleton — chính là INSTANCE).

Lỗi số 5: Ngoại lệ chưa xử lý. writeReplace/readResolve có thể ném ra ObjectStreamException. Hãy xử lý hoặc khai báo ném ngoại lệ này.

1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Nexus Vũ trụ: Đảm bảo tính duy nhất của sự tồn tại
Nexus Vũ trụ: Đảm bảo tính duy nhất của sự tồn tại
1
Nhiệm vụ
JAVA 25 SELF, mức độ, bài học
Đã khóa
Kho Lưu Trữ Dữ Liệu Bí Mật: Che Giấu Thông Tin Khi Truyền
Kho Lưu Trữ Dữ Liệu Bí Mật: Che Giấu Thông Tin Khi Truyền
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION