JavaRush /Курсы /JAVA 25 SELF /Настройка поведения сериализации: кастомные методы

Настройка поведения сериализации: кастомные методы

JAVA 25 SELF
43 уровень , 3 лекция
Открыта

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).

Схема:

flowchart LR A[Объект] -- writeReplace --> B[Прокси-объект] B -- writeObject --> C[Поток байтов] C -- readObject --> D[Прокси-объект] D -- readResolve --> E[Итоговый объект]

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. Обрабатывайте или явно прокидывайте его.

1
Задача
JAVA 25 SELF, 43 уровень, 3 лекция
Недоступна
Космический Нексус: Обеспечение Единства Существования
Космический Нексус: Обеспечение Единства Существования
1
Задача
JAVA 25 SELF, 43 уровень, 3 лекция
Недоступна
Хранилище Тайных Данных: Маскировка Информации При Передаче
Хранилище Тайных Данных: Маскировка Информации При Передаче
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Zlyden' Уровень 63
12 декабря 2025
Примитивные примеры не раскрывают суть понаписанного в этой лекции, после прочтения в голове кроме "нафига козе баян?" ничего не крутится. Про вискарь синглтон отдельное здрасьте от тех, кто его ещё не знает. Зачем в примере и задачке класс, который в себе не хранит ничего, кроме ссылки на себя? С фантазией у авторов вроде было не плохо, можно ж было более приближенный к жизни пример найти?
I'll kick them all Уровень 5
10 октября 2025

Простая аналогия:
Это как если бы вы отправляли посылку другу, но вместо себя в коробку положили бы игрушечного двойника. 
А когда друг распаковывает посылку, вместо игрушки у него в руках оказываетесь вы — настоящий! 
(В реальной жизни так не работает, но в Java — вполне.)
а где такое курево продается?