1. Серіалізація колекцій усередині колекцій
У Java колекції можуть містити не лише прості об’єкти (наприклад, String), а й інші колекції або об’єкти. Це відкриває можливість створювати складні структури: наприклад, Map<String, List<User>>, де User — ваш власний клас.
Приклад: серіалізація Map із вкладеним List
Розгляньмо приклад невеликої соціальної мережі, де в кожного користувача є список друзів.
import java.io.*;
import java.util.*;
class User implements Serializable {
private static final long serialVersionUID = 1L;
String name;
User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" + "name='" + name + '\'' + '}';
}
}
public class SocialNetwork implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<User>> friends = new HashMap<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
SocialNetwork network = new SocialNetwork();
network.friends.put("alice", Arrays.asList(new User("bob"), new User("carol")));
network.friends.put("bob", Collections.singletonList(new User("alice")));
// Серіалізація
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("network.ser"))) {
out.writeObject(network);
}
// Десеріалізація
SocialNetwork loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("network.ser"))) {
loaded = (SocialNetwork) in.readObject();
}
System.out.println("Відновлена мережа: " + loaded.friends);
}
}
Можна сказати так: усі використані типи — чи то HashMap, ArrayList або User — реалізують інтерфейс Serializable. Під час серіалізації Java автоматично проходить усі вкладені колекції та об’єкти, записуючи їх також. Тому після десеріалізації ви отримуєте повністю відновлену структуру, включно з усіма вкладеними списками.
Виведення:
Відновлена мережа: {alice=[User{name='bob'}, User{name='carol'}], bob=[User{name='alice'}]}
Вкладеність на будь-який смак
Ви можете створювати скільки завгодно рівнів вкладеності: List<List<User>>, Map<String, Map<Integer, List<User>>> — Java не боїться рекурсії (у межах розумного, звісно).
2. Ієрархічні об’єкти: серіалізація колекцій із наслідуванням
Що, якщо ваші колекції містять об’єкти, побудовані за принципом наслідування? Наприклад, у вас є базовий клас Animal, а в колекції містяться як Cat, так і Dog?
Приклад: серіалізація колекції з нащадками
import java.io.*;
import java.util.*;
abstract class Animal implements Serializable {
private static final long serialVersionUID = 1L;
String name;
Animal(String name) {
this.name = name;
}
public abstract String speak();
}
class Cat extends Animal {
private static final long serialVersionUID = 1L;
Cat(String name) {
super(name);
}
@Override
public String speak() {
return "Meow!";
}
}
class Dog extends Animal {
private static final long serialVersionUID = 1L;
Dog(String name) {
super(name);
}
@Override
public String speak() {
return "Woof!";
}
}
public class Zoo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
List<Animal> animals = new ArrayList<>();
animals.add(new Cat("Мурка"));
animals.add(new Dog("Шарик"));
// Серіалізація
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("zoo.ser"))) {
out.writeObject(animals);
}
// Десеріалізація
List<Animal> loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("zoo.ser"))) {
loaded = (List<Animal>) in.readObject();
}
for (Animal animal : loaded) {
System.out.println(animal.name + " каже: " + animal.speak());
}
}
}
Результат:
Мурка каже: Meow!
Шарик каже: Woof!
Важливий момент: Java серіалізує не лише поля базового класу, а й інформацію про реальний тип об’єкта. Тому після десеріалізації об’єкти зберігають свою «котячу» або «собачу» сутність, і ви можете безпечно викликати їхні методи.
3. Серіалізація графів об’єктів
Тепер час перейти до справжньої магії — серіалізації графів об’єктів, де об’єкти можуть посилатися один на одного, а не лише бути вкладеними один в один. Але для початку розберімося, що це за графи.
Що таке граф об’єктів?
Граф об’єктів — це структура, де об’єкти можуть бути пов’язані між собою через поля-посилання. Наприклад, у родовідному дереві в кожної людини можуть бути посилання на батьків, дітей, братів та сестер.
Аналогія: Уявіть собі групу друзів у соцмережі: у кожного користувача є список друзів, і ці друзі — теж користувачі, у яких свої друзі, і так далі. Це і є граф об’єктів.
Приклад: серіалізація двозв’язного списку
import java.io.*;
class Node implements Serializable {
private static final long serialVersionUID = 1L;
String value;
Node next;
Node prev;
Node(String value) {
this.value = value;
}
}
public class DoublyLinkedListDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
// Створюємо два пов’язані вузли
Node first = new Node("A");
Node second = new Node("B");
first.next = second;
second.prev = first;
// Серіалізація
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("list.ser"))) {
out.writeObject(first);
}
// Десеріалізація
Node loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("list.ser"))) {
loaded = (Node) in.readObject();
}
System.out.println("Значення першого: " + loaded.value); // "A"
System.out.println("Наступний: " + loaded.next.value); // "B"
System.out.println("Попередній у наступного: " + loaded.next.prev.value); // "A"
}
}
Зверніть увагу: серіалізується лише одне посилання (first), але завдяки рекурсивній серіалізації Java «пройде» всі пов’язані об’єкти. Під час десеріалізації структура посилань повністю відновиться: loaded.next.prev == loaded буде true! І якщо в графі є цикли (наприклад, коли вузли посилаються один на одного) — стандартна серіалізація Java працює коректно й не зациклюється.
4. Вкладені та ієрархічні колекції: приклад із реальним класом
Модель: каталог книжок
Нехай у нас є клас Book, який може бути паперовою книжкою або електронним виданням (наслідування). Також є клас Library, який містить мапу жанрів (Map<String, List<Book>>). Кожен жанр — це список книжок.
import java.io.*;
import java.util.*;
abstract class Book implements Serializable {
private static final long serialVersionUID = 1L;
String title;
Book(String title) {
this.title = title;
}
}
class PaperBook extends Book {
private static final long serialVersionUID = 1L;
int pages;
PaperBook(String title, int pages) {
super(title);
this.pages = pages;
}
}
class EBook extends Book {
private static final long serialVersionUID = 1L;
String format;
EBook(String title, String format) {
super(title);
this.format = format;
}
}
class Library implements Serializable {
private static final long serialVersionUID = 1L;
Map<String, List<Book>> catalog = new HashMap<>();
}
public class CatalogDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Library library = new Library();
library.catalog.put("Фантастика", Arrays.asList(
new PaperBook("Дюна", 800),
new EBook("Марсіанин", "epub")
));
library.catalog.put("Класика", Collections.singletonList(
new PaperBook("Війна і мир", 1200)
));
// Серіалізація
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("library.ser"))) {
out.writeObject(library);
}
// Десеріалізація
Library loaded;
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("library.ser"))) {
loaded = (Library) in.readObject();
}
for (Map.Entry<String, List<Book>> entry : loaded.catalog.entrySet()) {
System.out.println("Жанр: " + entry.getKey());
for (Book book : entry.getValue()) {
System.out.println(" - " + book.title + " (" + book.getClass().getSimpleName() + ")");
}
}
}
}
Виведення:
Жанр: Фантастика
- Дюна (PaperBook)
- Марсіанин (EBook)
Жанр: Класика
- Війна і мир (PaperBook)
Підсумок:
- Серіалізація вкладених колекцій (Map<String, List<Book>>) працює «з коробки».
- Типи об’єктів (PaperBook, EBook) зберігаються.
- Після десеріалізації структура повністю відновлюється.
5. Серіалізація графів об’єктів: що відбувається «під капотом»?
Коли ви серіалізуєте об’єкт, Java «проходить» усі його поля (і поля полів, і так далі), серіалізуючи кожен об’єкт лише один раз. Якщо об’єкт зустрічається повторно (наприклад, у циклічному посиланні), Java записує спеціальне посилання, а не серіалізує його знову.
Візуалізація (блок-схема)
graph TD
A[Об’єкт A] -- поле --> B[Об’єкт B]
B -- поле --> C[Об’єкт C]
C -- поле --> A
Java спочатку серіалізує A, потім B, потім C, а коли знову зустрічає A, вона записує «посилання на вже серіалізований об’єкт A». Під час десеріалізації структура відновлюється зі збереженням усіх зв’язків.
6. Особливості серіалізації графів
- Циклів не варто боятися: стандартна серіалізація Java підтримує циклічні посилання, не зациклюється і не спричиняє StackOverflowError.
- Усі об’єкти мають бути серіалізовуваними: якщо хоча б один об’єкт у графі не реалізує Serializable, серіалізація завершиться помилкою на цьому об’єкті.
- Однакові об’єкти не дублюються: якщо один і той самий об’єкт зустрічається в кількох місцях графа, після десеріалізації це буде той самий об’єкт (за посиланням).
- Типи об’єктів зберігаються: навіть якщо колекцію оголошено як List<Animal>, після десеріалізації ви отримаєте об’єкти їхніх реальних класів (Cat, Dog тощо).
7. Типові помилки під час серіалізації вкладених та ієрархічних об’єктів
Помилка № 1: Не всі класи серіалізовувані.
Дуже часто забувають додати implements Serializable до одного з власних класів, який лежить усередині колекції або вкладеного об’єкта. У результаті — NotSerializableException і розчарування. Перевіряйте ланцюжок вкладеності!
Помилка № 2: Втрата посилань під час ручної серіалізації.
Якщо ви реалізуєте методи writeObject/readObject самостійно і забуваєте серіалізувати одне з полів (наприклад, посилання на батька або на вкладену колекцію), після десеріалізації структура буде пошкоджена. Завжди тестуйте відновлення.
Помилка № 3: Використання transient для потрібних полів.
Якщо позначити потрібне поле як transient, воно не потрапить у серіалізований потік, і після відновлення буде null або матиме значення за замовчуванням. Це може порушити цілісність графа об’єктів.
Помилка № 4: Зміна структури класів між серіалізацією і десеріалізацією.
Якщо ви змінили структуру класу (наприклад, додали поле) після того, як серіалізували об’єкт, під час спроби десеріалізації можливі помилки або втрата даних. Використовуйте serialVersionUID і підтримуйте сумісність.
Помилка № 5: Серіалізація великих графів.
Складні взаємопов’язані структури можуть призвести до дуже великих файлів і тривалої серіалізації/десеріалізації. Слідкуйте за розмірами і за можливості розбивайте на частини.
Помилка № 6: Серіалізація «сирих» колекцій.
Якщо ви оголосили колекцію без generic‑параметра (наприклад, просто List), після десеріалізації доведеться явно приводити типи, що загрожує ClassCastException. Використовуйте дженерики та перевіряйте типи.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ