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. Використовуйте дженерики та перевіряйте типи.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ