JavaRush /Курси /JAVA 25 SELF /Серіалізація вкладених об’єктів: особливості

Серіалізація вкладених об’єктів: особливості

JAVA 25 SELF
Рівень 44 , Лекція 0
Відкрита

1. Вступ: серіалізація об’єктів із полями‑посиланнями

У реальних застосунках рідко трапляються повністю «плоскі» класи. Зазвичай об’єкт містить інші об’єкти, а ті, своєю чергою, можуть містити ще щось. Це називається композицією (або вкладеністю) об’єктів. Наприклад:

public class Address {
    String city;
    String street;
}

public class Person {
    String name;
    int age;
    Address address; // Вкладений об’єкт!
}

Коли ми серіалізуємо такий об’єкт, виникає запитання: а що робити з полем address? Чи має Java серіалізувати його разом із Person? А якщо в Address усередині є ще якийсь об’єкт? На щастя (або на жаль — залежно від ситуації), Java типово серіалізує всі вкладені об’єкти рекурсивно, якщо вони також реалізують інтерфейс Serializable.

Серіалізація в Java — це завжди глибока серіалізація (deep serialization). Це означає, що серіалізується не лише сам об’єкт, а й усі об’єкти, на які він посилається через свої (не transient) поля, і так далі — до самого «дна».

Візуалізація процесу

graph TD A[Person] --> B[Address] B --> C[CityInfo] A --> D[Pet]

Словом, якщо вирішили серіалізувати Person, то Java серіалізує і Address, і все, що всередині Address, і так далі.

2. Вимоги до вкладених об’єктів: Serializable — обов’язковий!

Ви вже, ймовірно, звернули увагу на важливий нюанс: усі вкладені об’єкти, які серіалізуються, також мають реалізовувати інтерфейс Serializable.

Якщо хоча б одне поле‑посилання вказує на об’єкт, який не реалізує Serializable, спроба серіалізації завершиться винятком java.io.NotSerializableException.

Розгляньмо приклади глибокої серіалізації.

Приклад: усе добре

import java.io.Serializable;

public class Address implements Serializable {
    String city;
    String street;
}

public class Person implements Serializable {
    String name;
    int age;
    Address address;
}

Обидва класи реалізують Serializable. Усе працює, серіалізація відбувається успішно.

Приклад: помилка!

public class Address { // НЕ реалізує Serializable!
    String city;
    String street;
}

public class Person implements Serializable {
    String name;
    int age;
    Address address;
}

Як бачимо, тут Address не реалізує інтерфейс Serializable. Тож під час серіалізації Person ми отримаємо виняток NotSerializableException.

3. Приклад: серіалізація і десеріалізація з вкладеним об’єктом

Погляньмо, як це виглядає на практиці. На минулому рівні ми працювали із застосунком «Менеджер контактів». Додаймо тепер до нього адресу кожного користувача.

import java.io.*;

class Address implements Serializable {
    String city;
    String street;

    Address(String city, String street) {
        this.city = city;
        this.street = street;
    }
}

class Person implements Serializable {
    String name;
    int age;
    Address address;

    Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
}

public class SerializationDemo {
    public static void main(String[] args) throws Exception {
        Person p = new Person("Іван", 30, new Address("Прага", "Славінська, 1"));
        // Серіалізація
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p);
        out.close();

        // Десеріалізація
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name + ", " + restored.age + ", " +
                           restored.address.city + ", " + restored.address.street);
    }
}

Результат:

Іван, 30, Прага, Славінська, 1

Усе працює: вкладений об’єкт Address серіалізовано і відновлено разом із Person.

4. Що, якщо вкладений об’єкт не серіалізується?

Якщо спробувати серіалізувати об’єкт, у якому хоча б одне поле‑посилання вказує на об’єкт, що не реалізує Serializable, Java викине виняток уже під час спроби серіалізації.

Демонстрація помилки

class Address { // не Serializable!
    String city;
    String street;
}

class Person implements Serializable {
    String name;
    Address address;
}

public class Test {
    public static void main(String[] args) throws Exception {
        Person p = new Person();
        p.name = "Петро";
        p.address = new Address();
        p.address.city = "Деррі";
        p.address.street = "В’язів";
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p); // <-- БУДЕ ВИНЯТОК!
        out.close();
    }
}

Помилка:

java.io.NotSerializableException: Address

5. Transient для вкладених об’єктів

Що робити, якщо у вас є поле‑посилання на об’єкт, який не слід серіалізувати (наприклад, кеш, з’єднання з базою даних, тимчасовий об’єкт)? У такому разі оголосіть поле як transient. Тоді Java просто пропустить це поле під час серіалізації.

Приклад із transient

class Address { // не Serializable
    String city;
    String street;
}

class Person implements Serializable {
    String name;
    transient Address address; // transient!

    Person(String name, Address address) {
        this.name = name;
        this.address = address;
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        Address addr = new Address();
        addr.city = "Лос-Сантос";
        addr.street = "Малголланд Драйв";
        Person p = new Person("Саша", addr);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser"));
        out.writeObject(p);
        out.close();

        // Десеріалізація
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name); // "Саша"
        System.out.println(restored.address); // null!
    }
}

Результат:
Поле address після десеріалізації дорівнює null, оскільки воно transient.

6. Глибока вкладеність і рекурсія

Серіалізація працює рекурсивно: якщо у Person є поле Address, а в Address є поле CityInfo і так далі, серіалізатор буде «пірнати» все глибше, допоки не зустріне щось несеріалізовуване або поки не закінчиться пам’ять (жарт, але лише наполовину).

Важливо: циклічні посилання

Java‑серіалізатор вміє працювати з циклічними посиланнями. Якщо, наприклад, об’єкт має посилання на інший об’єкт, який своєю чергою посилається назад, серіалізатор не зациклиться, а акуратно збереже структуру.

class A implements Serializable {
    B b;
}
class B implements Serializable {
    A a;
}

Якщо створити об’єкти A і B, що посилаються один на одного, серіалізація не спричинить StackOverflowError — Java пам’ятає вже серіалізовані об’єкти.

7. Приклад: серіалізація об’єкта з вкладеним списком

Часто всередині об’єкта є колекції інших об’єктів. Наприклад, у користувача може бути список друзів:

import java.io.*;
import java.util.*;

class Person implements Serializable {
    String name;
    List<Person> friends;

    Person(String name) {
        this.name = name;
        this.friends = new ArrayList<>();
    }
}

public class FriendsSerialization {
    public static void main(String[] args) throws Exception {
        Person alice = new Person("Alice");
        Person bob = new Person("Bob");
        alice.friends.add(bob);

        // Серіалізація
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("friends.ser"));
        out.writeObject(alice);
        out.close();

        // Десеріалізація
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("friends.ser"));
        Person restored = (Person) in.readObject();
        in.close();

        System.out.println(restored.name); // Alice
        System.out.println(restored.friends.get(0).name); // Bob
    }
}

Важливо: Колекції стандартної бібліотеки Java (ArrayList, HashMap тощо) реалізують Serializable, тож усе працює одразу.

8. Типові помилки під час серіалізації вкладених об’єктів

Помилка № 1: Один із вкладених об’єктів не реалізує Serializable. Ви забули додати implements Serializable в один із вкладених класів. Підсумок — NotSerializableException під час першої спроби серіалізації. Переконайтеся, що всі класи у вашому ланцюжку серіалізації підтримують цей інтерфейс.

Помилка № 2: Несеріалізовуване поле не позначене як transient. Якщо у вас є поле, яке не серіалізується (наприклад, потік, з’єднання з базою, щось тимчасове), але ви не оголосили його як transient, серіалізація завершиться помилкою. Не забувайте про transient!

Помилка № 3: Невідповідність serialVersionUID у вкладених класах. Якщо ви явно оголошуєте serialVersionUID у вкладених класах і змінюєте їхню структуру, не забувайте оновлювати цей ідентифікатор — інакше можливі помилки під час десеріалізації.

Помилка № 4: Мутуючі (змінювані) вкладені об’єкти. Якщо ви серіалізуєте колекцію або об’єкт, який потім змінюється (наприклад, список друзів), після десеріалізації це вже буде «знімок» на момент серіалізації. Нові зміни в оригіналі жодним чином не позначаться на десеріалізованому об’єкті.

Помилка № 5: Серіалізація величезних графів об’єктів. Якщо у вас дуже складна структура з великою кількістю вкладених об’єктів, серіалізація може зайняти багато часу та пам’яті. Інколи краще серіалізувати лише ключові дані, а не всю структуру цілком.

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