JavaRush /Курси /JAVA 25 SELF /Externalizable: тонке налаштування серіалізації

Externalizable: тонке налаштування серіалізації

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

1. Вступ

У Java для серіалізації об’єктів найчастіше використовують інтерфейс Serializable. Він простий: достатньо реалізувати інтерфейс, і об’єкт можна записувати/читати за допомогою ObjectOutputStream/ObjectInputStream. Але іноді цього недостатньо:

  • Потрібно повністю контролювати, які поля і як серіалізуються.
  • Необхідно забезпечити сумісність між різними версіями класу.
  • Важливо зменшити розмір серіалізованого файлу або пришвидшити процес.

Для таких випадків у Java є інтерфейс Externalizable — більш «ручний» і гнучкий спосіб серіалізації.

Коротко:

  • Serializable — автоматична серіалізація: Java сама вирішує, що і як записувати.
  • Externalizable — ручна серіалізація: ви самі визначаєте, що і як зберігати/відновлювати.

2. Контракт Externalizable: реалізуємо writeExternal і readExternal

Щоб використовувати Externalizable, потрібно:

  1. Реалізувати інтерфейс java.io.Externalizable.
  2. Обов’язково реалізувати два методи:
    • void writeExternal(ObjectOutput out) throws IOException
    • void readExternal(ObjectInput in) throws IOException, ClassNotFoundException

Приклад:

import java.io.*;

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

    // Обов’язковий публічний конструктор без параметрів!
    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }

    @Override
    public String toString() {
        return name + " (" + age + ")";
    }
}

Важливо: розробник сам вирішує, які саме поля будуть серіалізуватися і в якому порядку. Але при цьому є одна обов’язкова вимога: клас повинен мати public конструктор без параметрів. Якщо його не буде, то під час десеріалізації програма викине виняток InvalidClassException.

3. Коли використовувати Externalizable?

Використовуйте Externalizable, якщо:

  • Потрібен повний контроль над форматом даних. Наприклад, хочете серіалізувати лише частину полів або серіалізувати їх в особливому порядку/форматі.
  • Оптимізація продуктивності та розміру файлу. Стандартна серіалізація додає службову інформацію (метадані, назви класів, типи тощо). З Externalizable ви записуєте лише необхідні дані.
  • Забезпечення зворотної сумісності. Якщо структура класу змінюється, можна вручну реалізувати логіку читання старих і нових версій даних.
  • Серіалізація нестандартних об’єктів. Наприклад, якщо у вас є поля, які не можна серіалізувати стандартним способом (наприклад, transient, volatile або складні структури).

Коли НЕ варто використовувати?

  • Якщо вам не потрібен повний контроль — використовуйте Serializable, це простіше й безпечніше.
  • Якщо ви не впевнені, що зможете підтримувати сумісність формату даних під час змін класу.

4. Плюси та мінуси Externalizable відносно Serializable

Плюси:

  • Повний контроль над серіалізацією. Ви самі вирішуєте, що і як писати/читати.
  • Компактність. Немає зайвих метаданих — лише ваші дані.
  • Швидкість. Менше даних — швидше запис/читання.
  • Гнучкість. Можна реалізувати підтримку різних версій формату, додати стиснення, шифрування тощо.

Мінуси:

  • Ручна реалізація — легко помилитися. Якщо переплутати порядок запису/читання, серіалізація «зламається» (буде помилка або некоректні дані).
  • Немає автоматичної підтримки transient, serialVersionUID. Усе потрібно продумувати та реалізовувати вручну.
  • Складніше підтримувати. Під час зміни структури класу потрібно не забути оновити методи серіалізації.
  • Обов’язковий публічний конструктор без параметрів.
  • Менше «магії» — більше відповідальності.

5. Приклади: серіалізація та десеріалізація простого об’єкта

Серіалізація об’єкта

User user = new User("Alice", 30);

try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
    out.writeObject(user);
}

Десеріалізація об’єкта

try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
    User loaded = (User) in.readObject();
    System.out.println(loaded); // Alice (30)
}

Увага: якщо ви зміните порядок запису/читання полів або забудете серіалізувати якесь поле, дані будуть некоректними! Методи writeExternal і readExternal мають бути суворо узгоджені за послідовністю операцій.

Приклад: серіалізуємо лише частину полів

public class SecretUser implements Externalizable {
    private String login;
    private transient String password; // transient не має значення для Externalizable

    public SecretUser() {}

    public SecretUser(String login, String password) {
        this.login = login;
        this.password = password;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(login);
        // Не серіалізуємо пароль!
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        login = in.readUTF();
        password = null; // пароль не відновлюємо
    }
}

6. Практика: порівняння розміру серіалізованого файлу

Порівняймо, скільки «важать» файли, серіалізовані через Serializable і через Externalizable.

Клас із Serializable

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

    public UserSerializable(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Клас із Externalizable

public class UserExternalizable implements Externalizable {
    private String name;
    private int age;

    public UserExternalizable() {}

    public UserExternalizable(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = in.readUTF();
        age = in.readInt();
    }
}

Код для порівняння

import java.io.*;

public class CompareSerialization {
    public static void main(String[] args) throws Exception {
        UserSerializable s = new UserSerializable("Bob", 25);
        UserExternalizable e = new UserExternalizable("Bob", 25);

        // Serializable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"))) {
            out.writeObject(s);
        }

        // Externalizable
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ext.bin"))) {
            out.writeObject(e);
        }

        System.out.println("Serializable file size: " + new File("ser.bin").length());
        System.out.println("Externalizable file size: " + new File("ext.bin").length());
    }
}

Результат:
Файл ser.bin (Serializable) зазвичай більший — містить службову інформацію Java. Файл ext.bin (Externalizable) — лише ваші дані, зазвичай менший.

7. Типові помилки під час роботи з Externalizable

Помилка № 1: Відсутній публічний конструктор без параметрів.
Клас, що реалізує Externalizable, обов’язково повинен мати public конструктор без аргументів. Без нього десеріалізація призведе до винятку InvalidClassException.

Помилка № 2: Порушення порядку запису та читання полів.
Методи writeExternal і readExternal мають працювати в одному й тому самому порядку. Якщо під час запису спочатку зберегти поле name, а під час читання спочатку спробувати зчитати age, дані будуть спотворені.

Помилка № 3: Пропущені поля під час серіалізації.
Якщо забути записати поле в writeExternal, під час десеріалізації воно матиме значення null (для посилальних типів) або 0 (для числових).

Помилка № 4: Неправильне використання transient або serialVersionUID.
На відміну від Serializable, для Externalizable ці механізми автоматично не працюють — ви маєте самі контролювати, які поля зберігати, а які ні.

Помилка № 5: Зміна структури класу без оновлення методів.
Якщо додати або видалити поля і не внести відповідні зміни в writeExternal і readExternal, старі збережені дані можуть перестати коректно завантажуватися.

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