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 или не сериализуйте их вручную.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ