JavaRush /Курсы /JAVA 25 SELF /Вложенные и иерархические объекты: сериализация графов

Вложенные и иерархические объекты: сериализация графов

JAVA 25 SELF
44 уровень , 1 лекция
Открыта

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 поддерживает циклические ссылки, не зацикливается и не вызывает 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. Используйте дженерики и проверяйте типы.

1
Задача
JAVA 25 SELF, 44 уровень, 1 лекция
Недоступна
Картографирование подземных тоннелей с циклическим маршрутом 🚇
Картографирование подземных тоннелей с циклическим маршрутом 🚇
1
Задача
JAVA 25 SELF, 44 уровень, 1 лекция
Недоступна
Управление ассортиментом гипермаркета: иерархия и коллекции товаров 🏪
Управление ассортиментом гипермаркета: иерархия и коллекции товаров 🏪
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ