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

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