JavaRush /Курси /JAVA 25 SELF /Інтерфейс Serializable: базові принципи

Інтерфейс Serializable: базові принципи

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

1. Інтерфейс Serializable

Пам’ятаєте приклади з попередньої лекції? Класи Java, які ми хочемо серіалізувати, мають реалізовувати спеціальний інтерфейс — java.io.Serializable. Це так званий маркерний інтерфейс: він не містить жодного методу, а просто «позначає» клас як придатний до серіалізації. Якщо клас реалізує цей інтерфейс, JVM дозволяє серіалізувати його об’єкти стандартними засобами.

Серіалізувати все підряд не варто, тому що не всі об’єкти можна або потрібно перетворювати на байти. Деякі об’єкти залежать від стану операційної системи, відкритих файлів або мережевих з’єднань. Тому Java вимагає явного позначення класу як серіалізовного.

Маркерний інтерфейс — це як наклейка «дозволено до пакування» на коробці. Якщо такої наклейки немає — пакувальник (JVM) відмовляється працювати.

Приклад: оголошення серіалізовного класу

import java.io.Serializable;

public class User implements Serializable {
    private String name;
    private int age;

    // Конструктор, гетери й сетери
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Для краси: метод toString()
    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + "}";
    }
}

Зверніть увагу:

  • Ми просто додали implements Serializable до оголошення класу.
  • Не потрібно реалізовувати жодних методів (інтерфейс порожній).
  • Усі стандартні класи Java, які можна серіалізувати (наприклад, ArrayList, HashMap, String), уже реалізують Serializable.

2. Як зробити свій клас серіалізовним

Правило № 1: Просто додайте implements Serializable

Це все, що потрібно для самого класу. Але є нюанси!

Важливо: серіалізовними мають бути й усі вкладені об’єкти.
Якщо у вашому класі є поля-посилання на інші об’єкти, вони теж повинні бути серіалізовними. Наприклад:

public class Profile implements Serializable {
    private User user; // User має бути серіалізовним!
    private int level;
}

Якщо бодай одне з полів несеріалізовне, під час спроби серіалізації виникне виняток.

3. Приклад серіалізації та десеріалізації

Подивімося, як серіалізувати й десеріалізувати об’єкт у файл. Для цього використовуються класи ObjectOutputStream і ObjectInputStream.

Приклад: серіалізація об’єкта User у файл

import java.io.*;

public class SerializeDemo {
    public static void main(String[] args) {
        User user = new User("Alice", 30);

        // Зберігаємо об’єкт у файл
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("Об’єкт успішно серіалізовано у файл user.ser");
        } catch (IOException e) {
            System.out.println("Помилка серіалізації: " + e.getMessage());
        }
    }
}

Що тут відбувається?

  • Створюємо об’єкт User.
  • Відкриваємо потік ObjectOutputStream, який пише до файлу "user.ser".
  • Викликаємо writeObject(user). У цей момент JVM перетворює об’єкт на потік байтів і зберігає його у файл.

Приклад: десеріалізація об’єкта з файлу

import java.io.*;

public class DeserializeDemo {
    public static void main(String[] args) {
        // Читаємо об’єкт із файлу
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User user = (User) ois.readObject();
            System.out.println("Об’єкт успішно відновлено: " + user);
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Помилка десеріалізації: " + e.getMessage());
        }
    }
}

Що тут відбувається?

  • Відкриваємо потік ObjectInputStream, який читає з файлу "user.ser".
  • Викликаємо readObject(). JVM відновлює об’єкт із байтів.
  • Не забудьте виконати приведення результату до потрібного типу (User), адже readObject() повертає Object.
  • Може виникнути ClassNotFoundException, якщо клас User не знайдено під час десеріалізації.

Усе разом: серіалізація та десеріалізація

import java.io.*;

public class SerializationExample {
    public static void main(String[] args) {
        User user = new User("Bob", 22);

        // Серіалізація
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
            oos.writeObject(user);
            System.out.println("Серіалізацію завершено!");
        } catch (IOException e) {
            System.out.println("Помилка серіалізації: " + e.getMessage());
        }

        // Десеріалізація
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
            User loaded = (User) ois.readObject();
            System.out.println("Десеріалізацію завершено! " + loaded);
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("Помилка десеріалізації: " + e.getMessage());
        }
    }
}

Результат:

Серіалізацію завершено!
Десеріалізацію завершено! User{name='Bob', age=22}

4. Що відбувається «під капотом» під час серіалізації

Коли ви викликаєте writeObject, JVM спочатку перевіряє, чи реалізує клас інтерфейс Serializable. Якщо клас не позначено як серіалізовний, генерується виняток. Далі JVM проходить по всіх звичайних полях об’єкта (тобто тих, які не static і не transient) і записує їхні значення в потік байтів. Якщо серед цих полів є інші об’єкти, серіалізація застосовується до них рекурсивно, але лише якщо вони також реалізують Serializable.

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

Деякі поля не серіалізуються. Статичні поля (static) належать самому класу, а не окремому об’єкту, тому їхні значення не зберігаються. Поля, позначені як transient, теж пропускаються — це зручно для тимчасових даних, кешу або секретної інформації на кшталт паролів.

Схема процесу серіалізації

flowchart TB
    A[Об’єкт User у пам’яті] -- writeObject --> B[ObjectOutputStream]
    B -- зберігає байти --> C[Файл user.ser]
    C -- readObject --> D[ObjectInputStream]
    D -- відновлює --> E[Об’єкт User у пам’яті]

5. Типові помилки під час використання Serializable

Помилка № 1: Поле-посилання на несеріалізовний об’єкт. Якщо в класі User з’явиться поле типу, наприклад, Thread або Socket, серіалізація не спрацює. Не всі об’єкти можна серіалізувати — пам’ятайте про це!

Помилка № 2: Несеріалізовні вкладені класи. Якщо клас User містить внутрішній клас, який не є static, то серіалізація може не спрацювати. Краще використовувати static вкладені класи або окремі файли-класи.

Помилка № 3: Спроба серіалізувати static поле. Статичні поля не серіалізуються — вони належать класу, а не об’єкту. Після десеріалізації static-поле матиме значення, визначене в класі, а не в серіалізованому об’єкті.

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

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