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 записывает специальную ссылку, а не сериализует его снова.
Визуализация (блок-схема)
Java сначала сериализует A, затем B, затем C, а когда снова встречает A, она записывает «ссылку на уже сериализованный объект A». При десериализации структура восстанавливается с сохранением всех связей.
6. Особенности сериализации графов
- Циклы не страшны: стандартная сериализация Java поддерживает циклические ссылки, не зацикливается и не вызывает StackOverflow.
- Все объекты должны быть сериализуемыми: если хотя бы один объект в графе не реализует Serializable, сериализация провалится на этом объекте.
- Одинаковые объекты не дублируются: если один и тот же объект встречается в нескольких местах графа, после десериализации это будет один и тот же объект (по ссылке).
- Типы объектов сохраняются: даже если коллекция объявлена как List<Animal>, после десериализации вы получите объекты их реальных классов (Cat, Dog и т.д.).
7. Типичные ошибки при сериализации вложенных и иерархических объектов
Ошибка №1: Не все классы сериализуемы.
Очень часто забывают добавить implements Serializable в один из собственных классов, который лежит внутри коллекции или вложенного объекта. Как результат — NotSerializableException и разочарование. Проверяйте цепочку вложенности!
Ошибка №2: Потеря ссылок при ручной сериализации.
Если вы реализуете методы writeObject/readObject самостоятельно и забываете сериализовать одно из полей (например, ссылку на родителя или на вложенную коллекцию), после десериализации структура будет повреждена. Всегда тестируйте восстановление.
Ошибка №3: Использование transient для нужных полей.
Если пометить нужное поле как transient, оно не попадёт в сериализованный поток, и после восстановления будет null или иметь значение по умолчанию. Это может нарушить целостность графа объектов.
Ошибка №4: Смена структуры классов между сериализацией и десериализацией.
Если вы изменили структуру класса (например, добавили поле) после того, как сериализовали объект, при попытке десериализации возможны ошибки или потеря данных. Используйте serialVersionUID и поддерживайте совместимость.
Ошибка №5: Сериализация больших графов.
Сложные взаимосвязанные структуры могут привести к очень большим файлам и долгой сериализации/десериализации. Следите за размерами и по возможности разбивайте на части.
Ошибка №6: Сериализация «сырых» коллекций.
Если вы объявили коллекцию без generic-параметра (например, просто List), после десериализации придётся явно приводить типы, что чревато ClassCastException. Используйте дженерики и проверяйте типы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ