JavaRush /Курси /JAVA 25 SELF /Керування процесом серіалізації: writeObject, readObject

Керування процесом серіалізації: writeObject, readObject

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

1. Вступ

Автоматична серіалізація — це як автопілот у літаку: чудово працює, поки все йде за планом. Щойно з’являються особливі умови — стає зрозуміло, що простого механізму вже недостатньо. Уявіть, що потрібно зберегти об’єкт, але не всі його поля: якісь дані тимчасові, а якісь надто чутливі, щоб їх записувати у файл. Або навпаки — під час збереження потрібно додати щось своє: наприклад, версію чи контрольну суму. Буває й так, що перед записом або завантаженням даних потрібно виконати перевірку чи перетворення. А іноді завдання ще складніше: забезпечити сумісність із попередніми версіями класу, якщо його структура з часом змінилася.

Ось у таких ситуаціях і стає зрозуміло: однією стандартною серіалізацією не обійтися. Потрібно брати керування у власні руки.

Спеціальні методи серіалізації: writeObject і readObject

У Java є два спеціальні методи, які дають змогу повністю контролювати процес серіалізації та десеріалізації об’єкта:

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

Важливо!

  • Методи мають бути саме private (не public, не protected, не package-private).
  • Сигнатури мають точно збігатися з наведеними вище.
  • Якщо ці методи оголошені у вашому класі, вони будуть викликані замість стандартної серіалізації/десеріалізації.

Як це працює?

Коли ви викликаєте ObjectOutputStream.writeObject(obj), JVM спочатку шукає в класі obj метод private void writeObject(ObjectOutputStream). Якщо він є — викликається саме він. Аналогічно, під час десеріалізації викликається private void readObject(ObjectInputStream).

Якщо методи не оголошені, використовується стандартна серіалізація.

Як влаштовані writeObject і readObject

Сигнатури методів

private void writeObject(ObjectOutputStream out) throws IOException
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException

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

  • out.defaultWriteObject(); — для серіалізації стандартних (не transient) полів суперкласу та поточного класу.
  • in.defaultReadObject(); — для десеріалізації стандартних полів.

Якщо ви не викличете ці методи, то стандартні поля серіалізовані не будуть — і під час десеріалізації об’єкт виявиться «порожнім». Це як забути покласти паспорт у валізу: формально ви приїхали, але довести, хто ви, не зможете.

2. Приклад: додаємо контрольну суму під час серіалізації

Розгляньмо практичний приклад. Припустімо, у нас є клас користувача, і ми хочемо під час серіалізації додати до об’єкта контрольну суму, щоб під час десеріалізації перевірити цілісність даних.

import java.io.*;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // transient поле — його не серіалізуємо
    private transient int checksum;

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

    private int calculateChecksum() {
        return (name != null ? name.hashCode() : 0) + age;
    }

    // Власна серіалізація
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject(); // Зберігаємо стандартні поля
        int sum = calculateChecksum();
        out.writeInt(sum); // Записуємо контрольну суму
        System.out.println("[LOG] Серіалізація User: checksum=" + sum);
    }

    // Власна десеріалізація
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject(); // Відновлюємо стандартні поля
        int sum = in.readInt(); // Читаємо контрольну суму
        int actual = calculateChecksum();
        System.out.println("[LOG] Десеріалізація User: checksum=" + sum + ", actual=" + actual);
        if (sum != actual) {
            throw new IOException("Дані пошкоджено! Контрольна сума не збігається.");
        }
        this.checksum = actual;
    }

    @Override
    public String toString() {
        return "User{name='" + name + "', age=" + age + ", checksum=" + checksum + "}";
    }
}

Приклад використання:

// Зберігаємо об’єкт
User user = new User("Alice", 42);
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);
}

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

  • Під час серіалізації викликається writeObject, зберігаються стандартні поля + контрольна сума.
  • Під час десеріалізації викликається readObject, відновлюються поля + перевіряється контрольна сума.
  • У консолі з’явиться лог, і якщо щось не так — буде кинуто виняток.

3. Вилучення чутливих даних із серіалізації

Іноді потрібно, щоб певні поля не серіалізувалися (наприклад, паролі). Для цього можна використовувати ключове слово transient (про нього докладніше — у наступній лекції), але можна й вручну не серіалізувати поле, якщо ви реалізуєте writeObject.

Приклад:

public class Account implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;
    private transient String password; // transient — не серіалізується

    // Але можна зробити і так:
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        // Не записуємо password!
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        // password залишається null
    }
}

Увага:
Якщо ви хочете серіалізувати лише частину об’єкта — просто не записуйте зайві поля в потік.

4. Виклик методів суперкласу: defaultWriteObject і defaultReadObject

Усередині ваших методів writeObject і readObject майже завжди потрібно викликати defaultWriteObject() і defaultReadObject(). Це як натиснути «зберегти чернетку» перед тим, як додати власні нотатки.

Ці методи відповідають за стандартну серіалізацію всіх не-transient, не-static полів поточного класу та суперкласу. Якщо їх не викликати, ці поля не будуть серіалізовані й під час десеріалізації виявляться порожніми.

Приклад неправильної поведінки:

private void writeObject(ObjectOutputStream out) throws IOException {
    // out.defaultWriteObject(); // забули викликати!
    out.writeInt(123); // щось своє
}

У цьому випадку стандартні поля просто не збережуться!

5. Практика: логування процесу серіалізації

Додаймо логування до нашого класу користувача, щоб бачити, коли відбуваються серіалізація та десеріалізація.

public class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    private void writeObject(ObjectOutputStream out) throws IOException {
        System.out.println("[LOG] Серіалізація Person: " + name + ", вік " + age);
        out.defaultWriteObject();
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        System.out.println("[LOG] Десеріалізація Person: " + name + ", вік " + age);
    }
}

Використання:

Person p = new Person("Bob", 30);
// Зберігаємо у файл
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"))) {
    out.writeObject(p);
}
// Завантажуємо з файлу
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"))) {
    Person loaded = (Person) in.readObject();
}

Результат:
У консолі ви побачите повідомлення про те, що об’єкт серіалізується та десеріалізується.

6. Типові помилки при використанні writeObject/readObject

Помилка № 1: Не викликано defaultWriteObject/defaultReadObject. Якщо забути викликати ці методи, стандартні поля не будуть серіалізовані, і об’єкт після десеріалізації виявиться порожнім або некоректним.

Помилка № 2: Неправильна сигнатура методів. Методи мають бути суворо private void writeObject(ObjectOutputStream) і private void readObject(ObjectInputStream). Якщо зробити їх public/protected або змінити параметри — вони не будуть викликані автоматично.

Помилка № 3: Виняток у методі. Якщо в writeObject або readObject станеться виняток, серіалізація або десеріалізація перерветься, і об’єкт не буде коректно збережений/завантажений.

Помилка № 4: Пропущена серіалізація/десеріалізація суперкласу. Якщо ваш клас успадковується від іншого серіалізованого класу, обов’язково викликайте defaultWriteObject/defaultReadObject, інакше поля суперкласу не збережуться.

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

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