1. Методы writeReplace и readResolve: теория
Иногда стандартных средств сериализации недостаточно. Представьте ситуацию: у вас есть синглтон (класс, у которого может быть только один экземпляр во всей программе), и вы хотите, чтобы после десериализации он остался единственным экземпляром (а не появился новый клон). Или вы хотите сериализовать не сам объект, а его «облегчённую» версию (прокси), чтобы скрыть детали реализации или сэкономить место.
В Java для этого существуют специальные методы: writeReplace и readResolve. Их задача — заменить сериализуемый или десериализуемый объект на другой.
Простая аналогия:
Это как если бы вы отправляли посылку другу, но вместо себя в коробку положили бы игрушечного двойника. А когда друг распаковывает посылку, вместо игрушки у него в руках оказываетесь вы — настоящий! (В реальной жизни так не работает, но в Java — вполне.)
writeReplace
Метод private Object writeReplace() вызывается у объекта перед сериализацией. Он может вернуть любой объект, который будет реально сериализован вместо исходного. Если не реализован — сериализуется сам объект.
Сигнатура:
private Object writeReplace() throws ObjectStreamException
readResolve
Метод private Object readResolve() вызывается у объекта после десериализации. Он позволяет заменить только что созданный объект на другой (например, вернуть синглтон или кэшированный экземпляр).
Сигнатура:
private Object readResolve() throws ObjectStreamException
Важно:
Оба метода должны быть private и возвращать Object. Это требование спецификации сериализации Java. Если сделать их public — сериализация их просто проигнорирует.
2. Применение writeReplace и readResolve на практике
Синглтон и readResolve
Синглтон — это просто класс, у которого может быть только один экземпляр во всей программе. Если такой объект сериализовать и потом восстановить, то без метода readResolve появится новый экземпляр, и правило «единственности» нарушится. А с readResolve можно вернуть именно тот самый объект, сохранив идею синглтона.
import java.io.*;
public class MySingleton implements Serializable {
private static final MySingleton INSTANCE = new MySingleton();
private MySingleton() {}
public static MySingleton getInstance() {
return INSTANCE;
}
// Гарантируем, что после десериализации вернётся именно INSTANCE
private Object readResolve() throws ObjectStreamException {
return INSTANCE;
}
}
Пояснение:
Без readResolve после десериализации появится новый объект, не равный (по ==) оригинальному синглтону. С readResolve — всегда возвращается INSTANCE.
Проверим на практике:
MySingleton s1 = MySingleton.getInstance();
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.bin"));
out.writeObject(s1);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.bin"));
MySingleton s2 = (MySingleton) in.readObject();
in.close();
System.out.println(s1 == s2); // true, если есть readResolve; false — без него
writeReplace: сериализация прокси-объекта
Иногда объект слишком тяжёлый для сериализации, или содержит чувствительные данные, или просто не должен попадать наружу в полном виде. В этом случае можно сериализовать «заменитель» — прокси-объект.
Пример:
Допустим, у нас есть класс User с приватным паролем. Мы не хотим, чтобы пароль сериализовался.
import java.io.*;
public class User implements Serializable {
private String username;
private transient String password; // transient — не сериализуется
public User(String username, String password) {
this.username = username;
this.password = password;
}
// Вместо User сериализуем только UserProxy
private Object writeReplace() throws ObjectStreamException {
return new UserProxy(username);
}
// Прокси-класс — только для сериализации
private static class UserProxy implements Serializable {
private String username;
public UserProxy(String username) {
this.username = username;
}
private Object readResolve() throws ObjectStreamException {
// В реальной жизни пароль не восстановить — возвращаем User с пустым паролем
return new User(username, "");
}
}
}
Пояснение:
- При сериализации User превращается в UserProxy (без пароля).
- При десериализации UserProxy превращается обратно в User (но пароль уже пустой).
3. Кастомизация сериализации для иммутабельных объектов
Иммутабельные (неизменяемые) объекты часто используют приватные final-поля и не имеют сеттеров. При стандартной сериализации Java может обойти это ограничение, но иногда лучше явно контролировать процесс через writeReplace/readResolve.
Пример: Value Object
import java.io.*;
public final class Money implements Serializable {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object writeReplace() throws ObjectStreamException {
return new MoneyProxy(amount, currency);
}
private static class MoneyProxy implements Serializable {
private final int amount;
private final String currency;
MoneyProxy(int amount, String currency) {
this.amount = amount;
this.currency = currency;
}
private Object readResolve() throws ObjectStreamException {
return new Money(amount, currency);
}
}
}
Пояснение:
- При сериализации Money превращается в MoneyProxy (POJO).
- При десериализации MoneyProxy превращается обратно в Money.
Взаимодействие с writeObject/readObject
Методы writeReplace/readResolve работают независимо от writeObject/readObject. Если оба механизма определены, то сначала вызывается writeReplace, а уже у возвращённого объекта — writeObject (если он реализует Serializable).
Схема:
4. Практика: сериализация с подменой объекта
Давайте добавим кастомную сериализацию в ваше учебное приложение — например, для класса Person, чтобы при сериализации записывалось только имя, а возраст игнорировался (допустим, мы заботимся о приватности).
Шаг 1. Основной класс
import java.io.*;
public class Person implements Serializable {
private String name;
private int age; // не хотим сериализовать
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private Object writeReplace() throws ObjectStreamException {
return new PersonProxy(name);
}
private static class PersonProxy implements Serializable {
private final String name;
PersonProxy(String name) {
this.name = name;
}
private Object readResolve() throws ObjectStreamException {
return new Person(name, -1); // -1 — "возраст неизвестен"
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
Шаг 2. Тестируем
public class TestCustomSerialization {
public static void main(String[] args) throws Exception {
Person original = new Person("Alice", 30);
// Сериализация
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.bin"));
out.writeObject(original);
out.close();
// Десериализация
ObjectInputStream in = new ObjectInputStream(new FileInputStream("person.bin"));
Person deserialized = (Person) in.readObject();
in.close();
System.out.println("До сериализации: " + original);
System.out.println("После десериализации: " + deserialized);
}
}
Результат:
До сериализации: Person{name='Alice', age=30}
После десериализации: Person{name='Alice', age=-1}
Как видите, возраст не сериализовался — всё по плану!
5. Особенности и нюансы
Когда использовать writeReplace/readResolve?
- Когда нужно сериализовать только часть состояния объекта.
- Для сериализации/десериализации прокси-объектов.
- Для поддержки паттерна Singleton.
- Для иммутабельных или сложных объектов, чья внутренняя структура может меняться.
Когда не стоит использовать?
- Если можно обойтись transient-полями или writeObject/readObject.
- Если объект не должен подменяться на другой.
Совместимость с наследованием
Если суперкласс определяет writeReplace/readResolve, они будут вызваны и для подклассов (если не переопределены). Будьте аккуратны с иерархиями!
6. Типичные ошибки при кастомной сериализации
Ошибка №1: Неправильная видимость методов. Если сделать writeReplace/readResolve не private, сериализация их не вызовет. Только private!
Ошибка №2: Несовпадение возвращаемых типов. writeReplace/readResolve должны возвращать Object. Даже если фактически возвращаете свой тип — метод объявляйте с возвращаемым типом Object.
Ошибка №3: Потеря данных. Если прокси-объект не содержит всех нужных данных для восстановления исходного объекта, часть информации потеряется. Всегда проверяйте, что вы сможете восстановить объект обратно.
Ошибка №4: Нарушение инвариантов. readResolve должен возвращать объект, который соответствует ожиданиям программы (например, для синглтона — именно INSTANCE).
Ошибка №5: Необработанные исключения. writeReplace/readResolve могут выбрасывать ObjectStreamException. Обрабатывайте или явно прокидывайте его.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ