JavaRush /Курсы /JAVA 25 SELF /Контроль над процессом сериализации: writeObject, readObj...

Контроль над процессом сериализации: 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 или не сериализуйте их вручную.

1
Задача
JAVA 25 SELF, 43 уровень, 0 лекция
Недоступна
Сущность Волшебного Кота: Сохранение и Пробуждение с Журналированием
Сущность Волшебного Кота: Сохранение и Пробуждение с Журналированием
1
Задача
JAVA 25 SELF, 43 уровень, 0 лекция
Недоступна
Профиль Игрока: Сохранение Сути, Забвение Деталей
Профиль Игрока: Сохранение Сути, Забвение Деталей
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Andrey Уровень 1
9 октября 2025
43