CodeGym /Các khóa học /JAVA 25 SELF /Tuần tự hóa các collection generics: những đặc điểm

Tuần tự hóa các collection generics: những đặc điểm

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

1. Tuần tự hóa và giải tuần tự các collection generics

Generics (kiểu tổng quát) trong Java không phải là phép màu mà là một “ảo ảnh” do trình biên dịch hỗ trợ. Ở giai đoạn biên dịch, thông tin về tham số kiểu của generics bị xóa (gọi là type erasure, hay “xóa kiểu”). Nghĩa là trong thời gian chạy (runtime) collection List<String> không khác gì List<Object> hoặc List<Integer>. Tất cả chúng chỉ là List, và JVM không biết chính xác những kiểu nào đang được chứa bên trong.

Hãy xem ví dụ:

List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();

System.out.println(stringList.getClass() == intList.getClass()); // true!

Ở đây cơ chế khá tinh vi. Chúng ta tạo hai danh sách — một cho chuỗi, một cho số. Ở mức biên dịch, Java kiểm tra chặt chẽ để bạn không đưa vào List<String> thứ gì ngoài chuỗi, và vào List<Integer> thứ gì ngoài số. Nhưng ngay khi chương trình chạy, khác biệt biến mất. Với JVM, cả hai đối tượng đều chỉ là ArrayList, và nó không thể kiểm tra các phần tử cần phải thuộc kiểu nào. Đó là lý do so sánh lớp của hai danh sách (stringList.getClass() == intList.getClass()) trả về true.

Từ đó rút ra kết luận quan trọng: generics trong Java chủ yếu phục vụ tính tiện lợi và an toàn ở giai đoạn biên dịch. Nhưng ở runtime, các “nhãn” này bị mất. Vì vậy, nếu bạn tuần tự hóa một collection, vào file sẽ chỉ có dữ liệu, chứ không có thông tin về kiểu generic. Tức là danh sách giá trị được lưu, nhưng qua file không thể biết đó chính xác là List<String>, hay List<Object> hoặc List<Integer>.

Một ví dụ nữa: tuần tự hóa và giải tuần tự List<String>

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

public class GenericSerializationDemo {
    public static void main(String[] args) throws Exception {
        List<String> fruits = new ArrayList<>();
        fruits.add("Táo");
        fruits.add("Chuối");
        fruits.add("Cam");

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

        // Giải tuần tự
        List<String> loadedFruits;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("fruits.ser"))) {
            loadedFruits = (List<String>) ois.readObject();
        }

        System.out.println(loadedFruits); // [Táo, Chuối, Cam]
    }
}

Lưu ý dòng sau:

loadedFruits = (List<String>) ois.readObject();

Ở đây chúng ta ép kiểu kết quả thành List<String>, dù thực tế ở runtime nó chỉ là ArrayList. Trình biên dịch không thể kiểm tra đây thực sự là danh sách chuỗi; nếu bên trong không phải chuỗi — bạn sẽ gặp ClassCastException khi chương trình chạy.

2. Các vấn đề khi giải tuần tự các collection generics

Mất thông tin về kiểu của phần tử

Vì thông tin về tham số generic bị xóa, sau khi giải tuần tự Java không thể đảm bảo trong collection là đúng những đối tượng bạn kỳ vọng. Tất cả những gì bạn nhận được — là một collection “thô” (raw type), trình biên dịch sẽ không phàn nàn, nhưng vấn đề có thể lộ ra ở runtime.

Minh họa vấn đề

List rawList = new ArrayList();
rawList.add("Mèo");
rawList.add(42); // Integer!
// Giải tuần tự
List<String> loadedCats = (List<String>) ois.readObject();
String cat = loadedCats.get(1); // ClassCastException!

Cảnh báo unchecked cast

Trình biên dịch sẽ thẳng thắn cảnh báo về rủi ro tiềm ẩn:

Note: GenericSerializationDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Cảnh báo này cho biết bạn đang ép kiểu không kiểm tra, và trong collection có thể tồn tại các đối tượng kiểu ngoài dự kiến.

3. Đặc điểm tuần tự hóa các collection generics

Không có thông tin về tham số generic trong file

Khi bạn tuần tự hóa List<String>List<Integer>, trong file sẽ không có thông tin rằng đó là chuỗi hay số. Nội dung collection được tuần tự hóa “nguyên trạng” — các đối tượng theo thứ tự.

Nếu bạn mở file đã tuần tự hóa trong trình soạn thảo văn bản, bạn sẽ không thấy bất kỳ chữ nào về <String> hay <Integer>. Tất cả điều đó chỉ tồn tại ở mức mã nguồn và trình biên dịch.

Ví dụ: tuần tự hóa các collection khác nhau

List<Integer> numbers = Arrays.asList(1, 2, 3);
List<String> words = Arrays.asList("một", "hai", "ba");

// Tuần tự hóa
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"))) {
    oos.writeObject(numbers);
    oos.writeObject(words);
}

Trong file test.ser — chỉ có hai đối tượng kiểu ArrayList, hoàn toàn không có thông tin về tham số generic.

Vấn đề khi giải tuần tự collection “thô” (raw type)

Nếu bạn đã tuần tự hóa danh sách không có tham số generic (raw type), nhưng lại giải tuần tự như List<String>, trình biên dịch không thể kiểm tra tính đúng đắn của kiểu và có thể phát sinh lỗi ở runtime.

4. Best practices khi tuần tự hóa collection generics

Ghi rõ kiểu phần tử kỳ vọng.
Nếu API của bạn tuần tự hóa một collection, hãy nêu rõ kiểu phần tử được kỳ vọng. Ví dụ: “Phương thức này trả về List<User> đã được tuần tự hóa”.

Kiểm tra kiểu phần tử sau khi giải tuần tự.
Sau khi giải tuần tự collection, nên kiểm tra rằng tất cả phần tử có kiểu như mong đợi (đặc biệt nếu nguồn dữ liệu không do bạn kiểm soát).

for (Object obj : loadedList) {
    if (!(obj instanceof String)) {
        throw new IllegalStateException("Mong đợi chuỗi, nhưng tìm thấy: " + obj.getClass());
    }
}

Sử dụng collection bất biến.
Nếu bạn tuần tự hóa collection chỉ để đọc, hãy dùng collection bất biến — List.copyOf, Collections.unmodifiableList. Điều này giúp tránh việc vô tình thay đổi dữ liệu sau khi giải tuần tự.

Không trộn lẫn nhiều kiểu trong cùng một collection.
Cố gắng không tuần tự hóa các collection có phần tử nhiều kiểu khác nhau (ví dụ, List<Object> chứa nhiều lớp khác nhau). Điều này làm phức tạp quá trình giải tuần tự và có thể dẫn tới lỗi.

Thận trọng khi dùng suppress cảnh báo.
Nếu bạn chắc chắn rằng mình đang giải tuần tự collection với kiểu phần tử đúng, có thể suppress cảnh báo của trình biên dịch bằng annotation @SuppressWarnings("unchecked"):

@SuppressWarnings("unchecked")
List<String> loaded = (List<String>) ois.readObject();

Nhưng hãy dùng có ý thức — rất dễ che giấu vấn đề cho tới khi lên production.

5. Ví dụ: tuần tự hóa và giải tuần tự collection với lớp tự định nghĩa

Giả sử chúng ta có lớp User:

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

Tuần tự hóa danh sách người dùng:

List<User> users = Arrays.asList(
    new User("Alisa", 30),
    new User("Bob", 25)
);

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

// Giải tuần tự
List<User> loadedUsers;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("users.ser"))) {
    loadedUsers = (List<User>) ois.readObject();
}

System.out.println(loadedUsers); // [Alisa (30), Bob (25)]

Mọi thứ hoạt động! Nhưng nếu ai đó chèn vào file đã tuần tự hóa một đối tượng thuộc lớp khác, bạn có thể gặp ClassCastException khi cố đọc phần tử như User.

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

Collection có thể lồng nhau, ví dụ: List<List<String>>, Map<String, List<User>> v.v. Java tuần tự hóa các cấu trúc như vậy theo cách đệ quy, nhưng quy tắc vẫn giữ nguyên:

  • Tất cả collection lồng nhau và phần tử của chúng phải có thể tuần tự hóa.
  • Thông tin về tham số generic vẫn bị xóa.

Ví dụ: tuần tự hóa danh sách các danh sách

List<List<String>> matrix = new ArrayList<>();
matrix.add(Arrays.asList("a", "b"));
matrix.add(Arrays.asList("c", "d"));

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

// Giải tuần tự
List<List<String>> loadedMatrix;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("matrix.ser"))) {
    loadedMatrix = (List<List<String>>) ois.readObject();
}

System.out.println(loadedMatrix); // [[a, b], [c, d]]

7. Những điểm hữu ích

Tuần tự hóa collection generics với các triển khai khác nhau

Đôi khi bạn tuần tự hóa bằng một triển khai, nhưng lại giải tuần tự như một triển khai khác. Ví dụ, đã tuần tự hóa ArrayList, nhưng giải tuần tự thành LinkedList. Điều này sẽ dẫn tới lỗi ép kiểu:

List<String> list = new ArrayList<>();
// ...
List<String> loaded = (LinkedList<String>) ois.readObject(); // ClassCastException!

Gợi ý: Luôn giải tuần tự về đúng kiểu đã được tuần tự hóa, hoặc sử dụng interface (List) nếu bạn không quan tâm tới triển khai cụ thể.

Sử dụng thư viện (ví dụ: Gson, Jackson)

Các thư viện cho JSON (ví dụ, Gson, Jackson) có thể tuần tự hóa/giải tuần tự các collection với generics, nhưng cần chỉ rõ kiểu khi giải tuần tự do cơ chế xóa kiểu. Ví dụ cho Gson:

Type type = new com.google.gson.reflect.TypeToken<List<User>>(){}.getType();
List<User> users = gson.fromJson(json, type);

8. Generics và tuần tự hóa trong Map và Set

Tất cả các quy tắc ở trên cũng đúng với những collection khác có generics:

  • Khi tuần tự hóa Map<String, Integer>, thông tin về kiểu của khóa và giá trị không được lưu.
  • Khi giải tuần tự, bạn phải ép về kiểu cần thiết và cẩn trọng với nội dung.

Ví dụ: tuần tự hóa Map

Map<String, Integer> scores = new HashMap<>();
scores.put("Vasya", 90);
scores.put("Petya", 85);

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

// Giải tuần tự
Map<String, Integer> loadedScores;
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("scores.ser"))) {
    loadedScores = (Map<String, Integer>) ois.readObject();
}

System.out.println(loadedScores); // {Vasya=90, Petya=85}

9. Các lỗi thường gặp khi tuần tự hóa collection generics

Lỗi №1: ClassCastException khi giải tuần tự. Nếu bạn giải tuần tự collection như List<String>, nhưng bên trong có đối tượng thuộc kiểu khác, bạn sẽ gặp ClassCastException ở runtime. Luôn kiểm tra nội dung collection!

Lỗi №2: NotSerializableException do phần tử không thể tuần tự hóa. Nếu chỉ một phần tử trong collection không triển khai Serializable, quá trình tuần tự hóa sẽ thất bại với lỗi NotSerializableException. Hãy kiểm tra khả năng tuần tự hóa của tất cả các lớp có thể xuất hiện trong collection.

Lỗi №3: Mất thông tin về tham số generic. Sau khi giải tuần tự, đừng dựa vào tham số generic — ở runtime không còn chúng nữa. Hãy dùng kiểm tra kiểu tường minh nếu bạn nghi ngờ về tính đúng đắn của dữ liệu.

Lỗi №4: Không khớp giữa các triển khai collection. Đã tuần tự hóa ArrayList, nhưng lại giải tuần tự thành LinkedList — bạn sẽ nhận lỗi ép kiểu. Cố gắng giải tuần tự về đúng kiểu đã tuần tự hóa.

Lỗi №5: Không tương thích phiên bản lớp. Nếu cấu trúc lớp của phần tử trong collection thay đổi sau khi tuần tự hóa (ví dụ, thêm trường), có thể xuất hiện lỗi khi giải tuần tự. Hãy dùng serialVersionUID để kiểm soát phiên bản.

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