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).
GO TO FULL VERSION