CodeGym /Các khóa học /JAVA 25 SELF /Phân tích các lỗi thường gặp khi tuần tự hóa (serializati...

Phân tích các lỗi thường gặp khi tuần tự hóa (serialization) các collection

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

1. NotSerializableException: khi collection không thể được tuần tự hóa

Lỗi thường gặp nhất và “khó chịu” nhất khi tuần tự hóa collection là java.io.NotSerializableException. Nó xảy ra nếu ít nhất một phần tử của collection không triển khai giao diện Serializable.

Hãy xem một ví dụ đơn giản:

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

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

public class LibraryApp {
    public static void main(String[] args) throws Exception {
        List<Book> books = new ArrayList<>();
        books.add(new Book("Dombey and Son"));

        // Thử tuần tự hóa collection
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
            oos.writeObject(books); // BÙM! NotSerializableException
        }
    }
}

Điều gì sẽ xảy ra? Ở bước oos.writeObject(books) bạn sẽ nhận được ngoại lệ:

java.io.NotSerializableException: Book

Vì sao? Bởi vì lớp Book không triển khai giao diện Serializable. Ngay cả khi bản thân collection (ArrayList) có thể tuần tự hóa, các phần tử trong collection cũng phải có khả năng tuần tự hóa!

Cách chẩn đoán

Trong lỗi luôn có nêu lớp gây ra vấn đề — hãy tìm nó trong thông điệp ngoại lệ. Nếu collection lớn và lỗi chỉ xảy ra trong những điều kiện nhất định, có thể một phần tử nào đó được thêm vào tình cờ và không triển khai Serializable.

Cách khắc phục

Hãy thêm implements Serializable vào lớp của bạn:

class Book implements Serializable {
    String title;
    Book(String title) { this.title = title; }
}

Mẹo: Nếu collection chứa nhiều kiểu đối tượng khác nhau, hãy kiểm tra tất cả chúng có tuân theo Serializable!

2. ClassCastException khi giải tuần tự: khi generics gây rắc rối

Trong Java, thông tin về tham số generic của collection bị xóa sau khi biên dịch (type erasure). Điều đó có nghĩa là nếu bạn tuần tự hóa List<String>, nhưng lại giải tuần tự thành List<Integer>, trình biên dịch có thể không báo lỗi, nhưng khi chạy bạn sẽ nhận ClassCastException.

Ví dụ:

// Tuần tự hóa
List<String> names = Arrays.asList("Anna", "Boris");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("names.ser"))) {
    oos.writeObject(names);
}

// Giải tuần tự (NGUY HIỂM!)
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("names.ser"))) {
    List<Integer> numbers = (List<Integer>) ois.readObject(); // unchecked cast
    Integer first = numbers.get(0); // BÙM! ClassCastException
}

Lỗi:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer

Cách tránh

  • Đừng dùng collection “thô” (raw types) và đừng ép kiểu khi không cần thiết.
  • Nếu không chắc về nội dung, hãy kiểm tra kiểu phần tử sau khi giải tuần tự.
  • Hãy ghi rõ trong tài liệu kiểu collection được tuần tự hóa và kiểu mong đợi khi đọc.

Ví dụ giải tuần tự an toàn:

Object obj = ois.readObject();
if (obj instanceof List<?>) {
    List<?> list = (List<?>) obj;
    if (!list.isEmpty() && list.get(0) instanceof String) {
        @SuppressWarnings("unchecked")
        List<String> safeNames = (List<String>) obj; // đã suppress cảnh báo, nhưng kiểu đã được kiểm tra!
    }
}

3. Thay đổi cấu trúc lớp: serialVersionUID và khả năng tương thích ngược

Bạn đã tuần tự hóa collection, rồi quyết định thêm trường mới vào lớp phần tử, đổi tên trường hoặc thay đổi cấu trúc lớp. Bây giờ khi cố gắng giải tuần tự tệp cũ, bạn sẽ nhận lỗi “khó hiểu”:

java.io.InvalidClassException: Book; local class incompatible: stream classdesc serialVersionUID = 1234, local class serialVersionUID = 5678

Vì sao lại như vậy

Mỗi lớp có thể tuần tự hóa đều nhận một định danh phiên bản duy nhất — serialVersionUID. Nếu lớp thay đổi (ví dụ, bạn thêm một trường), JVM sẽ tính toán serialVersionUID mới, và quá trình giải tuần tự sẽ thấy phiên bản lớp không khớp với phiên bản khi tuần tự hóa.

Cách tránh

  • Khai báo rõ ràng serialVersionUID trong các lớp của bạn:
class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    String title;
    // ...
}
  • Giữ tương thích ngược: đừng xóa hoặc đổi tên trường nếu bạn dự định đọc các tệp cũ.
  • Hãy kiểm thử giải tuần tự sau mỗi thay đổi.

Phải làm gì nếu vẫn cần thay đổi lớp?

  • Cân nhắc hiện thực các phương thức readObject/writeObject để điều khiển thủ công quá trình tuần tự hóa.
  • Hoặc di trú dữ liệu: đọc tệp cũ bằng phiên bản lớp cũ, sau đó lưu lại theo định dạng mới.

4. Mất dữ liệu khi tuần tự hóa các collection bất biến

Trong các phiên bản Java mới xuất hiện các collection bất biến, ví dụ được tạo qua List.of(), Set.of(), Map.of(). Ở các phiên bản Java cũ (trước 12) và một số triển khai của bên thứ ba, việc tuần tự hóa các collection này có thể không hoạt động chính xác: sau khi giải tuần tự, collection trở thành có thể thay đổi hoặc thậm chí phát sinh lỗi.

Ví dụ:

List<String> list = List.of("a", "b", "c");
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
    oos.writeObject(list);
}

Trên một số JVM cũ, khi giải tuần tự sẽ xuất hiện lỗi hoặc collection không còn bất biến.

Cách tránh

  • Kiểm tra tài liệu của phiên bản Java bạn đang dùng.
  • Kiểm thử quá trình tuần tự hóa và giải tuần tự của các collection như vậy.
  • Nếu cần giữ tính bất biến, sau khi giải tuần tự hãy bọc collection bằng Collections.unmodifiableList(list).

5. Tuần tự hóa các trường transient và static

Điều gì xảy ra với các trường này:

  • transient — các trường được đánh dấu từ khóa này sẽ không được tuần tự hóa. Sau khi giải tuần tự, chúng sẽ có giá trị mặc định (ví dụ, null hoặc 0).
  • static — các trường của lớp (không phải của đối tượng) sẽ không bao giờ được tuần tự hóa.

Ví dụ:

class Book implements Serializable {
    String title;
    transient String cache; // không được tuần tự hóa!
    static String publisher = "Default"; // cũng không được tuần tự hóa!
}

Vì sao điều này quan trọng

Nếu bạn lưu trữ các giá trị tính toán hoặc bộ nhớ đệm bên trong đối tượng, hãy đánh dấu chúng là transient — điều này tiết kiệm dung lượng và tăng tốc quá trình tuần tự hóa.

Cẩn thận: Sau khi giải tuần tự, các trường transient cần được tính lại hoặc khởi tạo lại.

6. Tuần tự hóa các collection lớn: hiệu năng và kích thước tệp

Vấn đề:

  • Các collection lớn (ví dụ, hàng triệu đối tượng) có thể dẫn đến tệp rất lớn, thời gian ghi và đọc kéo dài, và đôi khi còn thiếu bộ nhớ (OutOfMemoryError).
  • Khi tuần tự hóa đồ thị đối tượng (ví dụ, các collection liên kết phức tạp), kích thước tệp có thể tăng đột biến ngoài dự kiến.

Cách tránh

  • Tuần tự hóa collection theo từng phần: ví dụ, ghi từng đối tượng hoặc theo các lô nhỏ.
  • Sử dụng xử lý theo luồng: thay vì tuần tự hóa toàn bộ collection ngay lập tức, hãy tuần tự hóa các phần tử theo nhu cầu.
  • Nén tệp: sử dụng GZIPOutputStream để giảm kích thước tệp.

Ví dụ tuần tự hóa theo luồng:

try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("books.ser"))) {
    for (Book book : bigList) {
        oos.writeObject(book);
    }
}

Lưu ý: Với cách tiếp cận này, khi giải tuần tự bạn cần biết có bao nhiêu đối tượng đã được ghi (hoặc sử dụng một “marker kết thúc” đặc biệt).

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Tuần tự hóa cấu trúc phức tạp
Tuần tự hóa cấu trúc phức tạp
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION