1. Вступ: серіалізація об’єктів із полями‑посиланнями
У реальних застосунках рідко трапляються повністю «плоскі» класи. Зазвичай об’єкт містить інші об’єкти, а ті, своєю чергою, можуть містити ще щось. Це називається композицією (або вкладеністю) об’єктів. Наприклад:
public class Address {
String city;
String street;
}
public class Person {
String name;
int age;
Address address; // Вкладений об’єкт!
}
Коли ми серіалізуємо такий об’єкт, виникає запитання: а що робити з полем address? Чи має Java серіалізувати його разом із Person? А якщо в Address усередині є ще якийсь об’єкт? На щастя (або на жаль — залежно від ситуації), Java типово серіалізує всі вкладені об’єкти рекурсивно, якщо вони також реалізують інтерфейс Serializable.
Серіалізація в Java — це завжди глибока серіалізація (deep serialization). Це означає, що серіалізується не лише сам об’єкт, а й усі об’єкти, на які він посилається через свої (не transient) поля, і так далі — до самого «дна».
Візуалізація процесу
Словом, якщо вирішили серіалізувати Person, то Java серіалізує і Address, і все, що всередині Address, і так далі.
2. Вимоги до вкладених об’єктів: Serializable — обов’язковий!
Ви вже, ймовірно, звернули увагу на важливий нюанс: усі вкладені об’єкти, які серіалізуються, також мають реалізовувати інтерфейс Serializable.
Якщо хоча б одне поле‑посилання вказує на об’єкт, який не реалізує Serializable, спроба серіалізації завершиться винятком java.io.NotSerializableException.
Розгляньмо приклади глибокої серіалізації.
Приклад: усе добре
import java.io.Serializable;
public class Address implements Serializable {
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Обидва класи реалізують Serializable. Усе працює, серіалізація відбувається успішно.
Приклад: помилка!
public class Address { // НЕ реалізує Serializable!
String city;
String street;
}
public class Person implements Serializable {
String name;
int age;
Address address;
}
Як бачимо, тут Address не реалізує інтерфейс Serializable. Тож під час серіалізації Person ми отримаємо виняток NotSerializableException.
3. Приклад: серіалізація і десеріалізація з вкладеним об’єктом
Погляньмо, як це виглядає на практиці. На минулому рівні ми працювали із застосунком «Менеджер контактів». Додаймо тепер до нього адресу кожного користувача.
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("Іван", 30, new Address("Прага", "Славінська, 1"));
// Серіалізація
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Десеріалізація
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);
}
}
Результат:
Іван, 30, Прага, Славінська, 1
Усе працює: вкладений об’єкт Address серіалізовано і відновлено разом із Person.
4. Що, якщо вкладений об’єкт не серіалізується?
Якщо спробувати серіалізувати об’єкт, у якому хоча б одне поле‑посилання вказує на об’єкт, що не реалізує Serializable, Java викине виняток уже під час спроби серіалізації.
Демонстрація помилки
class Address { // не 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 = "Петро";
p.address = new Address();
p.address.city = "Деррі";
p.address.street = "В’язів";
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p); // <-- БУДЕ ВИНЯТОК!
out.close();
}
}
Помилка:
java.io.NotSerializableException: Address
5. Transient для вкладених об’єктів
Що робити, якщо у вас є поле‑посилання на об’єкт, який не слід серіалізувати (наприклад, кеш, з’єднання з базою даних, тимчасовий об’єкт)? У такому разі оголосіть поле як transient. Тоді Java просто пропустить це поле під час серіалізації.
Приклад із transient
class Address { // не 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 = "Лос-Сантос";
addr.street = "Малголланд Драйв";
Person p = new Person("Саша", addr);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
out.writeObject(p);
out.close();
// Десеріалізація
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
Person restored = (Person) in.readObject();
in.close();
System.out.println(restored.name); // "Саша"
System.out.println(restored.address); // null!
}
}
Результат:
Поле address після десеріалізації дорівнює null, оскільки воно transient.
6. Глибока вкладеність і рекурсія
Серіалізація працює рекурсивно: якщо у Person є поле Address, а в Address є поле CityInfo і так далі, серіалізатор буде «пірнати» все глибше, допоки не зустріне щось несеріалізовуване або поки не закінчиться пам’ять (жарт, але лише наполовину).
Важливо: циклічні посилання
Java‑серіалізатор вміє працювати з циклічними посиланнями. Якщо, наприклад, об’єкт має посилання на інший об’єкт, який своєю чергою посилається назад, серіалізатор не зациклиться, а акуратно збереже структуру.
class A implements Serializable {
B b;
}
class B implements Serializable {
A a;
}
Якщо створити об’єкти A і B, що посилаються один на одного, серіалізація не спричинить StackOverflowError — Java пам’ятає вже серіалізовані об’єкти.
7. Приклад: серіалізація об’єкта з вкладеним списком
Часто всередині об’єкта є колекції інших об’єктів. Наприклад, у користувача може бути список друзів:
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);
// Серіалізація
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
out.writeObject(alice);
out.close();
// Десеріалізація
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
}
}
Важливо: Колекції стандартної бібліотеки Java (ArrayList, HashMap тощо) реалізують Serializable, тож усе працює одразу.
8. Типові помилки під час серіалізації вкладених об’єктів
Помилка № 1: Один із вкладених об’єктів не реалізує Serializable. Ви забули додати implements Serializable в один із вкладених класів. Підсумок — NotSerializableException під час першої спроби серіалізації. Переконайтеся, що всі класи у вашому ланцюжку серіалізації підтримують цей інтерфейс.
Помилка № 2: Несеріалізовуване поле не позначене як transient. Якщо у вас є поле, яке не серіалізується (наприклад, потік, з’єднання з базою, щось тимчасове), але ви не оголосили його як transient, серіалізація завершиться помилкою. Не забувайте про transient!
Помилка № 3: Невідповідність serialVersionUID у вкладених класах. Якщо ви явно оголошуєте serialVersionUID у вкладених класах і змінюєте їхню структуру, не забувайте оновлювати цей ідентифікатор — інакше можливі помилки під час десеріалізації.
Помилка № 4: Мутуючі (змінювані) вкладені об’єкти. Якщо ви серіалізуєте колекцію або об’єкт, який потім змінюється (наприклад, список друзів), після десеріалізації це вже буде «знімок» на момент серіалізації. Нові зміни в оригіналі жодним чином не позначаться на десеріалізованому об’єкті.
Помилка № 5: Серіалізація величезних графів об’єктів. Якщо у вас дуже складна структура з великою кількістю вкладених об’єктів, серіалізація може зайняти багато часу та пам’яті. Інколи краще серіалізувати лише ключові дані, а не всю структуру цілком.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ