CodeGym /Các khóa học /JAVA 25 SELF /Định danh của đối tượng khi tuần tự hóa nhị phân

Định danh của đối tượng khi tuần tự hóa nhị phân

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

1. Giới thiệu

Trong Java (và nói chung trong OOP), mỗi đối tượng có định danh — không chỉ là tập giá trị các trường của nó, mà là “nó là ai” trong bộ nhớ. Hai đối tượng có thể “tương đương” (ví dụ, a.equals(b) trả về true), nhưng khác nhau về định danh (a != b). Định danh là khi hai biến cùng trỏ tới đúng một đối tượng trong bộ nhớ (a == b).

Tại sao điều này quan trọng đối với tuần tự hóa?

Khi bạn tuần tự hóa (lưu) một đồ thị đối tượng (ví dụ: cây, danh sách hoặc chỉ là hai đối tượng cùng trỏ tới một đối tượng lồng nhau), điều quan trọng là phải giữ lại không chỉ giá trị các trường mà còn cả cấu trúc các tham chiếu giữa những đối tượng.

Nếu trong đồ thị có tham chiếu lặp/dùng chung (nhiều đối tượng cùng trỏ tới một đối tượng lồng nhau) hoặc thậm chí tham chiếu vòng (A → B → A), thì sau khi giải tuần tự hóa, các liên kết này phải được giữ nguyên.

Ví dụ: bạn có hai đối tượng A và B, cả hai cùng trỏ tới một đối tượng C. Sau khi tuần tự hóa và giải tuần tự hóa, cần phải có: a.c == b.c (cùng một đối tượng trong bộ nhớ). Nếu việc tuần tự hóa chỉ “sao chép” đối tượng, thì sau khi khôi phục sẽ tạo ra hai đối tượng C khác nhau — và đó là một đồ thị khác.

2. ObjectOutputStream giải quyết vấn đề định danh như thế nào

Trong Java, để tuần tự hóa nhị phân có một cặp lớp: ObjectOutputStream (ghi) và ObjectInputStream (đọc).

ObjectOutputStream không chỉ đơn thuần là “ghi đối tượng vào tệp”. Nó thông minh:

  • Theo dõi tất cả các đối tượng đã được tuần tự hóa trong phạm vi một luồng ghi (một phiên).
  • Nếu gặp tham chiếu tới một đối tượng đã được tuần tự hóa, nó không ghi lại đối tượng đó, mà ghi một “tham chiếu lặp” đặc biệt (reference handle).
  • Khi giải tuần tự hóa, ObjectInputStream khôi phục cấu trúc sao cho các tham chiếu lặp cùng trỏ tới đúng một đối tượng trong bộ nhớ.

Điều này hoạt động ngay cả với các tham chiếu vòng! Tức là nếu bạn có A → B → A, quá trình tuần tự hóa và giải tuần tự hóa sẽ không bị lặp vô hạn hay “sập” — các tham chiếu sẽ được khôi phục chính xác.

Nó được hiện thực như thế nào?

Bên trong ObjectOutputStream có một bảng đặc biệt (identity map), nơi lưu các đối tượng đã được tuần tự hóa. Khi gặp một đối tượng mới, nó được thêm vào bảng và được tuần tự hóa đầy đủ. Khi gặp lại một đối tượng đã có trong bảng — vào luồng chỉ ghi “tham chiếu lặp” (handle).

3. Minh họa bằng ví dụ

Ví dụ 1: Đồ thị với tham chiếu vòng (A → B, B → A)

import java.io.*;

class Node implements Serializable {
    String name;
    Node next;

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

public class CyclicSerializationDemo {
    public static void main(String[] args) throws Exception {
        Node a = new Node("A");
        Node b = new Node("B");
        a.next = b;
        b.next = a; // Vòng lặp!

        // Tuần tự hóa
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cyclic.dat"))) {
            out.writeObject(a);
        }

        // Giải tuần tự hóa
        Node a2;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cyclic.dat"))) {
            a2 = (Node) in.readObject();
        }

        System.out.println("a2.name = " + a2.name); // A
        System.out.println("a2.next.name = " + a2.next.name); // B
        System.out.println("a2.next.next == a2: " + (a2.next.next == a2)); // true!
    }
}

Kết quả:

a2.name = A
a2.next.name = B
a2.next.next == a2: true

Bình luận:
Vòng lặp được giữ nguyên! Sau khi giải tuần tự hóa, a2.next.next lại trỏ tới a2.

Ví dụ 2: Tham chiếu dùng chung (A → C, B → C)

import java.io.*;

class Wrapper implements Serializable {
    String name;
    Object ref;

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

public class SharedReferenceDemo {
    public static void main(String[] args) throws Exception {
        Wrapper a = new Wrapper("A");
        Wrapper b = new Wrapper("B");
        Wrapper c = new Wrapper("C");

        a.ref = c;
        b.ref = c;

        // Tuần tự hóa
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("shared.dat"))) {
            out.writeObject(new Wrapper[] {a, b});
        }

        // Giải tuần tự hóa
        Wrapper[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("shared.dat"))) {
            arr = (Wrapper[]) in.readObject();
        }

        Wrapper a2 = arr[0];
        Wrapper b2 = arr[1];

        System.out.println("a2.ref == b2.ref: " + (a2.ref == b2.ref)); // true!
        System.out.println("a2.ref.name = " + ((Wrapper)a2.ref).name); // C
    }
}

Kết quả:

a2.ref == b2.ref: true
a2.ref.name = C

Bình luận:
Sau khi giải tuần tự hóa, cả hai tham chiếu lại cùng trỏ tới đúng một đối tượng.

4. Liên hệ với writeReplace và readResolve

Trong Java, bạn có thể “can thiệp” vào quá trình (giải) tuần tự hóa bằng các phương thức đặc biệt:

writeReplace() — được gọi trước khi tuần tự hóa đối tượng. Có thể trả về một đối tượng khác để tuần tự hóa thay cho đối tượng gốc.
readResolve() — được gọi sau khi giải tuần tự hóa đối tượng. Có thể trả về một đối tượng khác để sử dụng thay cho đối tượng vừa được tạo ra.

Ảnh hưởng đến định danh:

  • Nếu dùng writeReplace() hoặc readResolve(), thì chính đối tượng được các phương thức này trả về sẽ được đưa vào bảng tham chiếu.
  • Điều này có thể thay đổi định danh: nếu readResolve() trả về một đối tượng mới/khác, các tham chiếu lặp có thể trỏ tới những thực thể khác nhau.
  • Hãy cẩn thận: bạn có thể “làm hỏng” định danh, hoặc cố ý đảm bảo nó (ví dụ kinh điển — Singleton qua readResolve(), nơi mọi tham chiếu sau khi giải tuần tự hóa đều trỏ tới cùng một singleton).

5. Thực hành: mã cho thấy việc giữ nguyên định danh

import java.io.*;

class Shared implements Serializable {
    String value;
    Shared(String value) { this.value = value; }
}

class Holder implements Serializable {
    String name;
    Shared shared;
    Holder(String name, Shared shared) {
        this.name = name;
        this.shared = shared;
    }
}

public class IdentityDemo {
    public static void main(String[] args) throws Exception {
        Shared c = new Shared("C");
        Holder a = new Holder("A", c);
        Holder b = new Holder("B", c);

        // Tuần tự hóa
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("identity.dat"))) {
            out.writeObject(new Holder[] {a, b});
        }

        // Giải tuần tự hóa
        Holder[] arr;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("identity.dat"))) {
            arr = (Holder[]) in.readObject();
        }

        Holder a2 = arr[0];
        Holder b2 = arr[1];

        System.out.println("a2.shared == b2.shared: " + (a2.shared == b2.shared)); // true!
        System.out.println("a2.shared.value = " + a2.shared.value); // C
    }
}

Kết quả:

a2.shared == b2.shared: true
a2.shared.value = C

Bình luận:
Định danh của đối tượng c được giữ nguyên: sau khi giải tuần tự hóa, cả hai tham chiếu đều trỏ tới cùng một thực thể.

6. Tổng kết và lỗi thường gặp

Lỗi số 1: tuần tự hóa riêng rẽ các đối tượng có tham chiếu dùng chung. Nếu bạn ghi mỗi đối tượng bằng một thể hiện ObjectOutputStream khác nhau, các tham chiếu giữa chúng sẽ không được giữ — các thực thể sau khi giải tuần tự hóa sẽ khác nhau, ngay cả khi ban đầu đó là cùng một tham chiếu.

Lỗi số 2: sử dụng không đúng writeReplace()readResolve(). Các phương thức này có thể thay thế đối tượng trong quá trình (giải) tuần tự hóa, dẫn đến thay đổi định danh. Nếu không hiểu cơ chế, bạn có thể nhận được các thực thể ngoài mong đợi.

Lỗi số 3: hiệu ứng ngoài ý muốn từ các tham chiếu biến đổi dùng chung. Nếu nhiều đối tượng cùng trỏ tới một đối tượng lồng nhau có thể thay đổi (ví dụ, danh sách), sau khi giải tuần tự hóa, điều này vẫn giữ nguyên. Thay đổi ở một nơi sẽ ảnh hưởng tới tất cả nơi khác.

Lỗi số 4: kỳ vọng có “đối tượng mới” sau khi tuần tự hóa. Tuần tự hóa không tạo ra một cấu trúc “mới tinh” — nó khôi phục lại các tham chiếu như bản gốc. Điều này có thể gây bất ngờ khi làm việc với cache hoặc các đối tượng mẫu.

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