CodeGym /Các khóa học /JAVA 25 SELF /Đối tượng lồng nhau và có thứ bậc: tuần tự hóa đồ thị

Đối tượng lồng nhau và có thứ bậc: tuần tự hóa đồ thị

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

1. Tuần tự hóa các collection lồng nhau

Trong Java, các collection không chỉ có thể chứa kiểu đơn giản (ví dụ String) mà còn có thể chứa các collection khác hoặc đối tượng khác. Điều này mở ra khả năng tạo các cấu trúc phức tạp: ví dụ Map<String, List<User>>, trong đó User — lớp do bạn tự định nghĩa.

Ví dụ: Tuần tự hóa Map với List lồng bên trong

Hãy xem ví dụ một mạng xã hội nhỏ, nơi mỗi người dùng có một danh sách bạn bè.

import java.io.*;
import java.util.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

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

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + '}';
    }
}

public class SocialNetwork implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<User>> friends = new HashMap<>();

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SocialNetwork network = new SocialNetwork();
        network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
        network.friends.put("bob", Collections.singletonList(new User("alice")));

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

        // Giải tuần tự
        SocialNetwork loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
            loaded = (SocialNetwork) in.readObject();
        }

        System.out.println("Mạng đã được khôi phục: " + loaded.friends);
    }
}

Có thể nói như sau: tất cả các kiểu được sử dụng — dù là HashMap, ArrayList hay User — đều triển khai giao diện Serializable. Khi tuần tự hóa, Java tự động đi qua mọi collection và đối tượng lồng bên trong, ghi chúng lại luôn. Vì vậy sau khi giải tuần tự, bạn nhận được cấu trúc được khôi phục toàn vẹn, bao gồm mọi danh sách con.

Kết quả:

Mạng đã được khôi phục: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}

Mức độ lồng tùy thích

Bạn có thể tạo bao nhiêu mức lồng cũng được: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java không sợ đệ quy (dĩ nhiên là trong phạm vi hợp lý).

2. Đối tượng có thứ bậc: tuần tự hóa các bộ sưu tập có kế thừa

Điều gì xảy ra nếu các collection của bạn chứa những đối tượng được xây dựng theo nguyên tắc kế thừa? Ví dụ bạn có lớp cơ sở Animal, còn trong collection có cả Cat lẫn Dog?

Ví dụ: Tuần tự hóa bộ sưu tập chứa các lớp dẫn xuất

import java.io.*;
import java.util.*;

abstract class Animal implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;

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

    public abstract String speak();
}

class Cat extends Animal {
    private static final long serialVersionUID = 1L;

    Cat(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Meow!";
    }
}

class Dog extends Animal {
    private static final long serialVersionUID = 1L;

    Dog(String name) {
        super(name);
    }

    @Override
    public String speak() {
        return "Woof!";
    }
}

public class Zoo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        List<Animal> animals = new ArrayList<>();
        animals.add(new Cat("Murka"));
        animals.add(new Dog("Sharik"));

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

        // Giải tuần tự
        List<Animal> loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
            loaded = (List<Animal>) in.readObject();
        }

        for (Animal animal : loaded) {
            System.out.println(animal.name + " nói: " + animal.speak());
        }
    }
}

Kết quả:

Murka nói: Meow!
Sharik nói: Woof!

Điểm quan trọng: Java không chỉ tuần tự hóa các trường của lớp cơ sở mà còn cả thông tin về kiểu thực của đối tượng. Do đó sau khi giải tuần tự, các đối tượng vẫn giữ nguyên “bản chất” mèo hay chó của mình và bạn có thể gọi phương thức của chúng một cách an toàn.

3. Tuần tự hóa đồ thị đối tượng

Đã đến lúc chuyển sang phần “ma thuật” thực sự — tuần tự hóa các đồ thị đối tượng, nơi các đối tượng có thể tham chiếu lẫn nhau, chứ không chỉ lồng vào nhau. Nhưng trước tiên hãy hiểu rõ đồ thị đó là gì.

Đồ thị đối tượng là gì?

Đồ thị đối tượng là một cấu trúc trong đó các đối tượng có thể liên kết với nhau qua các trường tham chiếu. Ví dụ, trong cây gia phả, mỗi người có thể có tham chiếu đến cha mẹ, con cái, anh chị em.

Tương tự: Hãy hình dung một nhóm bạn trên mạng xã hội: mỗi người dùng có danh sách bạn bè, và những người bạn đó cũng là người dùng có danh sách bạn bè riêng, cứ thế tiếp diễn. Đó chính là đồ thị đối tượng.

Ví dụ: Tuần tự hóa danh sách liên kết kép

import java.io.*;

class Node implements Serializable {
    private static final long serialVersionUID = 1L;
    String value;
    Node next;
    Node prev;

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

public class DoublyLinkedListDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // Tạo hai nút liên kết
        Node first = new Node("A");
        Node second = new Node("B");
        first.next = second;
        second.prev = first;

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

        // Giải tuần tự
        Node loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
            loaded = (Node) in.readObject();
        }

        System.out.println("Giá trị của nút đầu tiên: " + loaded.value); // "A"
        System.out.println("Kế tiếp: " + loaded.next.value);   // "B"
        System.out.println("Phần tử trước của phần tử kế tiếp: " + loaded.next.prev.value); // "A"
    }
}

Lưu ý rằng chỉ một tham chiếu (first) được tuần tự hóa, nhưng nhờ cơ chế tuần tự hóa đệ quy, Java sẽ “đi qua” tất cả các đối tượng liên kết. Khi giải tuần tự, cấu trúc tham chiếu sẽ được khôi phục đầy đủ: loaded.next.prev == loaded sẽ là true! Và nếu trong đồ thị có vòng lặp (ví dụ các nút tham chiếu lẫn nhau) thì cơ chế tuần tự hóa chuẩn của Java vẫn hoạt động chính xác và không bị lặp vô hạn.

4. Các bộ sưu tập lồng nhau và có thứ bậc: ví dụ với lớp thực tế

Mô hình: Danh mục sách

Giả sử ta có lớp Book, có thể là sách giấy thông thường hoặc ấn bản điện tử (kế thừa). Cũng có lớp Library, chứa bản đồ thể loại (Map<String, List<Book>>). Mỗi thể loại là một danh sách sách.

import java.io.*;
import java.util.*;

abstract class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;

    Book(String title) {
        this.title = title;
    }
}

class PaperBook extends Book {
    private static final long serialVersionUID = 1L;
    int pages;

    PaperBook(String title, int pages) {
        super(title);
        this.pages = pages;
    }
}

class EBook extends Book {
    private static final long serialVersionUID = 1L;
    String format;

    EBook(String title, String format) {
        super(title);
        this.format = format;
    }
}

class Library implements Serializable {
    private static final long serialVersionUID = 1L;
    Map<String, List<Book>> catalog = new HashMap<>();
}

public class CatalogDemo {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Library library = new Library();
        library.catalog.put("Fantastika", Arrays.asList(
                new PaperBook("Dyuna", 800),
                new EBook("Marsianin", "epub")
        ));
        library.catalog.put("Klassika", Collections.singletonList(
                new PaperBook("Voina i mir", 1200)
        ));

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

        // Giải tuần tự
        Library loaded;
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
            loaded = (Library) in.readObject();
        }

        for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
            System.out.println("Thể loại: " + entry.getKey());
            for (Book book : entry.getValue()) {
                System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
            }
        }
    }
}

Kết quả:

Thể loại: Fantastika
 - Dyuna (PaperBook)
 - Marsianin (EBook)
Thể loại: Klassika
 - Voina i mir (PaperBook)

Tổng kết:

  • Việc tuần tự hóa các collection lồng nhau (Map<String, List<Book>>) hoạt động ngay “out of the box”.
  • Kiểu thực của đối tượng (PaperBook, EBook) được giữ nguyên.
  • Sau khi giải tuần tự, cấu trúc được khôi phục hoàn toàn.

5. Tuần tự hóa đồ thị đối tượng: có gì xảy ra “bên dưới nắp máy”?

Khi bạn tuần tự hóa một đối tượng, Java sẽ “đi” qua tất cả các trường của nó (và các trường của các đối tượng con, v.v.), và chỉ tuần tự hóa mỗi đối tượng đúng một lần. Nếu một đối tượng xuất hiện lặp lại (ví dụ trong một vòng tham chiếu), Java sẽ ghi một tham chiếu đặc biệt, thay vì tuần tự hóa nó lần nữa.

Hình dung (lưu đồ)

graph TD
    A[Đối tượng A] -- trường --> B[Đối tượng B]
    B -- trường --> C[Đối tượng C]
    C -- trường --> A

Java sẽ tuần tự hóa A trước, sau đó đến B, rồi C; và khi gặp lại A, nó ghi “tham chiếu đến đối tượng A đã được tuần tự hóa trước đó”. Khi giải tuần tự, cấu trúc được khôi phục với đầy đủ các liên kết.

6. Đặc điểm của việc tuần tự hóa đồ thị

  • Vòng lặp không đáng ngại: cơ chế tuần tự hóa chuẩn của Java hỗ trợ các tham chiếu vòng, không lặp vô hạn và không gây ra StackOverflow.
  • Tất cả các đối tượng phải có thể tuần tự hóa: nếu chỉ một đối tượng trong đồ thị không triển khai Serializable, quá trình tuần tự hóa sẽ thất bại tại đối tượng đó.
  • Các đối tượng giống nhau không bị nhân đôi: nếu cùng một đối tượng xuất hiện ở nhiều nơi trong đồ thị, sau khi giải tuần tự, đó vẫn là cùng một đối tượng (theo tham chiếu).
  • Kiểu đối tượng được giữ nguyên: ngay cả khi collection được khai báo là List<Animal>, sau khi giải tuần tự bạn sẽ nhận được các đối tượng theo đúng lớp thực của chúng (Cat, Dog, v.v.).

7. Các lỗi thường gặp khi tuần tự hóa các đối tượng lồng nhau và có thứ bậc

Lỗi số 1: Không phải tất cả lớp đều có thể tuần tự hóa.
Rất thường xuyên người ta quên thêm implements Serializable vào một trong các lớp tự định nghĩa nằm bên trong collection hoặc đối tượng lồng nhau. Kết quả — NotSerializableException và sự thất vọng. Hãy kiểm tra chuỗi lồng nhau!

Lỗi số 2: Mất tham chiếu khi tự tay tuần tự hóa.
Nếu bạn tự triển khai các phương thức writeObject/readObject và quên tuần tự hóa một trong các trường (ví dụ tham chiếu đến cha hoặc một collection lồng bên trong), sau khi giải tuần tự cấu trúc sẽ bị hỏng. Luôn kiểm thử việc khôi phục.

Lỗi số 3: Dùng transient cho các trường cần thiết.
Nếu đánh dấu một trường cần thiết là transient, nó sẽ không được đưa vào luồng tuần tự, và sau khi khôi phục sẽ là null hoặc mang giá trị mặc định. Điều này có thể phá vỡ tính toàn vẹn của đồ thị đối tượng.

Lỗi số 4: Thay đổi cấu trúc lớp giữa lúc tuần tự hóa và giải tuần tự.
Nếu bạn thay đổi cấu trúc lớp (ví dụ thêm trường) sau khi đối tượng đã được tuần tự hóa, khi cố giải tuần tự có thể phát sinh lỗi hoặc mất dữ liệu. Hãy dùng serialVersionUID và duy trì khả năng tương thích.

Lỗi số 5: Tuần tự hóa các đồ thị lớn.
Các cấu trúc liên kết phức tạp có thể dẫn đến file rất lớn và thời gian tuần tự hóa/giải tuần tự dài. Hãy theo dõi kích thước và nếu có thể thì chia nhỏ.

Lỗi số 6: Tuần tự hóa collection “raw”.
Nếu bạn khai báo collection không kèm tham số generic (ví dụ chỉ List), sau khi giải tuần tự bạn sẽ phải ép kiểu tường minh, điều này dễ dẫn đến ClassCastException. Hãy dùng generic và kiểm tra kiểu dữ liệu.

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