1. Giới thiệu: tuần tự hóa các đối tượng có trường tham chiếu
Trong các ứng dụng thực tế, hiếm khi gặp các lớp hoàn toàn “phẳng”. Thông thường, một đối tượng chứa các đối tượng khác, và chúng lại có thể chứa những thành phần khác nữa. Điều này được gọi là composition (hay tính lồng nhau) của đối tượng. Ví dụ:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Đối tượng lồng nhau!
}
Khi chúng ta tuần tự hóa một đối tượng như vậy, câu hỏi đặt ra: phải làm gì với trường address? Java có nên tuần tự hóa nó cùng với Person không? Nếu bên trong Address lại có thêm một đối tượng khác thì sao? May mắn (hoặc đáng tiếc — tùy tình huống), theo mặc định Java sẽ tuần tự hóa đệ quy tất cả đối tượng lồng nhau, nếu chúng cũng triển khai giao diện Serializable.
Trong Java, tuần tự hóa luôn là tuần tự hóa sâu (deep serialization). Điều đó có nghĩa là không chỉ bản thân đối tượng được tuần tự hóa, mà cả mọi đối tượng mà nó tham chiếu thông qua các trường (không transient), và cứ như vậy — cho đến tận “đáy”.
Minh họa quy trình
graph TD
A[Person] --> B[Address]
B --> C[CityInfo]
A --> D[Pet]
Nói ngắn gọn, nếu bạn quyết định tuần tự hóa Person, thì Java sẽ tuần tự hóa cả Address, và mọi thứ bên trong Address, v.v.
2. Yêu cầu với đối tượng lồng nhau: Serializable là bắt buộc!
Có một điểm quan trọng: mọi đối tượng lồng nhau được tuần tự hóa cũng phải triển khai giao diện Serializable.
Nếu ít nhất một trường tham chiếu trỏ tới đối tượng không triển khai Serializable, thì nỗ lực tuần tự hóa sẽ kết thúc bằng ngoại lệ java.io.NotSerializableException.
Hãy xem các ví dụ về tuần tự hóa sâu.
Ví dụ: mọi thứ ổn
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Cả hai lớp đều triển khai Serializable. Mọi thứ hoạt động, tuần tự hóa thành công.
Ví dụ: lỗi!
public class Address { // KHÔNG triển khai Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Như thấy, Address không triển khai giao diện Serializable. Vì vậy, khi tuần tự hóa Person, chúng ta sẽ nhận ngoại lệ NotSerializableException.
3. Ví dụ: tuần tự hóa và giải tuần tự hóa với đối tượng lồng nhau
Hãy xem điều này trông như thế nào trong thực tế. Ở cấp trước, chúng ta đã làm việc với ứng dụng “Trình quản lý liên hệ”. Bây giờ hãy thêm địa chỉ cho mỗi người dùng.
import java.io.*;
class Address implements Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
}
class Person implements Serializable {
String name;
int age;
Address address;
Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}
}
public class SerializationDemo {
public static void main(String[] args) throws Exception {
Person p = new Person("Ivan", 30, new Address("Prague", "Slavinska, 1"));
// Tuần tự hóa
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Giải tuần tự hóa
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name + ", " + restored.age + ", " +
restored.address.city + ", " + restored.address.street);
}
}
Kết quả:
Ivan, 30, Prague, Slavinska, 1
Mọi thứ hoạt động: đối tượng lồng nhau Address được tuần tự hóa và khôi phục cùng với Person.
4. Điều gì xảy ra nếu đối tượng lồng nhau không thể tuần tự hóa?
Nếu cố gắng tuần tự hóa một đối tượng mà trong đó có ít nhất một trường tham chiếu trỏ tới đối tượng không triển khai Serializable, Java sẽ ném ngoại lệ ngay trong quá trình tuần tự hóa.
Minh họa lỗi
class Address { // không Serializable!
String city;
String street;
}
class Person implements Serializable {
String name;
Address address;
}
public class Test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.name = "Petya";
p.address = new Address();
p.address.city = "Derry";
p.address.street = "Vyazov";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- SẼ NÉM NGOẠI LỆ!
out.close();
}
}
Lỗi:
java.io.NotSerializableException: Address
5. Transient cho các đối tượng lồng nhau
Phải làm gì nếu bạn có một trường tham chiếu đến đối tượng không nên được tuần tự hóa (ví dụ: cache, kết nối cơ sở dữ liệu, đối tượng tạm thời)? Trong trường hợp đó, hãy khai báo trường là transient. Khi đó Java sẽ bỏ qua trường này khi tuần tự hóa.
Ví dụ với transient
class Address { // không Serializable
String city;
String street;
}
class Person implements Serializable {
String name;
transient Address address; // transient!
Person(String name, Address address) {
this.name = name;
this.address = address;
}
}
public class Test {
public static void main(String[] args) throws Exception {
Address addr = new Address();
addr.city = "Los Santos";
addr.street = "Mulholland Drive";
Person p = new Person("Sasha", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Giải tuần tự hóa
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Sasha"
System.out.println(restored.address); // null!
}
}
Kết quả:
Trường address sau khi giải tuần tự bằng null, vì nó là transient.
6. Độ lồng sâu và đệ quy
Tuần tự hóa hoạt động theo cách đệ quy: nếu Person có trường Address, và Address có trường CityInfo, v.v., bộ tuần tự hóa sẽ “đi sâu” cho đến khi gặp thứ gì đó không thể tuần tự hóa hoặc cho đến khi hết bộ nhớ (đùa thôi, nhưng cũng đúng một nửa).
Quan trọng: tham chiếu vòng
Bộ tuần tự hóa của Java xử lý được tham chiếu vòng. Nếu, ví dụ, một đối tượng tham chiếu đến đối tượng khác, và đối tượng đó lại tham chiếu ngược trở lại, bộ tuần tự hóa sẽ không bị lặp vô hạn mà vẫn lưu lại cấu trúc một cách an toàn.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Nếu tạo các đối tượng A và B tham chiếu lẫn nhau, tuần tự hóa sẽ không gây ra StackOverflowError — Java ghi nhớ các đối tượng đã được tuần tự hóa.
7. Ví dụ: tuần tự hóa đối tượng có danh sách lồng nhau
Thường bên trong một đối tượng có các collection của những đối tượng khác. Ví dụ, một người dùng có thể có danh sách bạn bè:
import java.io.*;
import java.util.*;
class Person implements Serializable {
String name;
List<Person> friends;
Person(String name) {
this.name = name;
this.friends = new ArrayList<>();
}
}
public class FriendsSerialization {
public static void main(String[] args) throws Exception {
Person alice = new Person("Alice");
Person bob = new Person("Bob");
alice.friends.add(bob);
// Tuần tự hóa
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Giải tuần tự hóa
ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // Alice
System.out.println(restored.friends.get(0).name); // Bob
}
}
Quan trọng: Các collection của thư viện chuẩn Java (ArrayList, HashMap và v.v.) triển khai Serializable, vì vậy mọi thứ hoạt động sẵn dùng.
8. Lỗi thường gặp khi tuần tự hóa đối tượng lồng nhau
Lỗi 1: Một trong các đối tượng lồng nhau không triển khai Serializable. Bạn quên thêm implements Serializable cho một trong các lớp lồng nhau. Kết quả — NotSerializableException ngay ở lần thử tuần tự hóa đầu tiên. Hãy kiểm tra rằng tất cả lớp trong chuỗi tuần tự hóa của bạn đều hỗ trợ giao diện này.
Lỗi 2: Trường không thể tuần tự hóa không được khai báo là transient. Nếu bạn có một trường không thể tuần tự hóa (ví dụ, luồng, kết nối cơ sở dữ liệu, thứ gì đó tạm thời) nhưng không khai báo nó là transient, quá trình tuần tự hóa sẽ thất bại với lỗi. Đừng quên transient!
Lỗi 3: serialVersionUID không khớp trong các lớp lồng nhau. Nếu bạn khai báo tường minh serialVersionUID trong các lớp lồng nhau và thay đổi cấu trúc của chúng, hãy nhớ cập nhật định danh này — nếu không, có thể phát sinh lỗi khi giải tuần tự hóa.
Lỗi 4: Các đối tượng lồng nhau có thể thay đổi (mutable). Nếu bạn tuần tự hóa một collection hay một đối tượng sau đó bị thay đổi (ví dụ, danh sách bạn bè), thì sau khi giải tuần tự hóa, đó chỉ là “ảnh chụp” tại thời điểm tuần tự hóa. Những thay đổi mới trong bản gốc sẽ không phản ánh vào đối tượng đã giải tuần tự.
Lỗi 5: Tuần tự hóa các đồ thị đối tượng khổng lồ. Nếu cấu trúc của bạn rất phức tạp với nhiều đối tượng lồng nhau, quá trình tuần tự hóa có thể tốn nhiều thời gian và bộ nhớ. Đôi khi tốt hơn nên chỉ tuần tự hóa dữ liệu cốt lõi thay vì toàn bộ cấu trúc.
GO TO FULL VERSION