Сериализация и десериализация объектов в Java - Вопросы

Всего: 5 вопросов

1. 

Что такое сериализация и десериализация в Java, зачем они нужны и как сериализовать объект?

Сериализация — преобразование состояния объекта в последовательность байтов, которую можно сохранить в файл или базу данных либо передать по сети. Десериализация — обратный процесс: восстановление объекта с тем же состоянием из этих байтов. Так сохраняют состояние программы между запусками (корзину, настройки) и передают объекты в другой процесс или на другую машину (Java RMI, распределённые кеши, HTTP-сессии в кластере).

Класс должен реализовать интерфейс-маркер java.io.Serializable (в нём нет методов), иначе будет брошено NotSerializableException. Порядок действий: создать FileOutputStream, обернуть его в ObjectOutputStream и вызвать writeObject(obj). Для чтения используется ObjectInputStream.readObject() с приведением к нужному типу; в читающей программе в classpath должен быть тот же класс с совместимым serialVersionUID.

Встроенная сериализация даёт бинарный формат, понятный только Java. Термин «маршалинг» шире — подготовка данных к передаче между системами (Java RMI, JAXB Marshaller для XML). Для обмена с другими системами обычно выбирают JSON (Jackson, Gson), XML или Protocol Buffers.

2. 

Что происходит с полями объекта при десериализации и как восстанавливается граф объектов?

JVM выделяет память и заполняет поля значениями из потока, не вызывая конструктор сериализуемого класса. Поэтому:

  • обычные поля (примитивы и Serializable-объекты, в том числе private) получают значения из потока;
  • transient-поля не пишутся в поток и получают значение по умолчанию (null, 0, false), даже если у поля есть инициализатор;
  • static-поля не пишутся в поток: видно текущее значение класса в этой JVM;
  • поля несериализуемого родителя заполняет его конструктор без параметров; если такого доступного конструктора нет — InvalidClassException: no valid constructor.

Записывается весь граф — все объекты, достижимые по ссылкам, и каждый из них должен быть сериализуемым. Если две ссылки указывали на один объект, после чтения они тоже указывают на один объект (desCollar1 == desCollar2 даёт true), но только в пределах одного ObjectOutputStream. Сам десериализованный объект всегда новый: dog1 == deserializedDogs[0] даёт false.

3. 

Что такое serialVersionUID и почему его лучше объявлять явно?

serialVersionUID — номер версии сериализуемого класса, который записывается в поток вместе с объектом. При чтении JVM сравнивает номер из потока с номером класса в classpath; если они различаются, readObject() бросает java.io.InvalidClassException: ... local class incompatible.

Если поле не объявлено, JVM вычисляет его по структуре класса, и любое изменение (даже новый метод) или другой компилятор может дать другое значение. Поэтому версию объявляют явно: private static final long serialVersionUID = 1L;, с Java 14 — с аннотацией @Serial, чтобы компилятор проверил объявление.

При неизменном явном номере совместимые изменения не ломают чтение старых данных: новое поле получит значение по умолчанию, лишнее поле из потока будет проигнорировано. Номер меняют, только когда старые данные намеренно нельзя читать (например, изменился тип поля).

4. 

Чем Externalizable отличается от Serializable и как частично управлять сериализацией без Externalizable?

Externalizable расширяет Serializable и отдаёт программисту полный контроль над форматом:

  • нужно реализовать writeExternal(ObjectOutput) и readExternal(ObjectInput); записывается только то, что явно записано в коде, поэтому transient роли не играет;
  • при чтении вызывается public-конструктор без параметров; без него — InvalidClassException;
  • у Serializable все нестатические и не-transient поля пишутся автоматически, а конструктор при чтении не вызывается.

Serializable выбирают почти всегда, Externalizable — когда нужен компактный или особый формат. Частичный контроль дают приватные методы writeObject(ObjectOutputStream) и readObject(ObjectInputStream) в самом классе: внутри вызывают defaultWriteObject()/defaultReadObject() и дописывают своё, например размер кольца из transient-поля. Для record (Java 16+) эти методы игнорируются: при чтении вызывается канонический конструктор, и его проверки выполняются.

5. 

Почему объект может не сериализоваться или не читаться обратно и чем опасна десериализация недоверенных данных?

Частые причины:

  • NotSerializableException при записи — в графе есть объект класса без Serializable: поле такого типа, экземпляр нестатического внутреннего или анонимного класса (хранит ссылку на внешний объект), лямбда в поле. Исправление — реализовать Serializable, пометить поле transient или сделать вложенный класс static;
  • InvalidClassException при чтении — изменился serialVersionUID или у несериализуемого родителя нет конструктора без параметров;
  • ClassNotFoundException — класса нет в classpath; EOFException — прочитано больше объектов, чем записано;
  • NullPointerException после чтения — поле было transient и стало null.

Десериализация недоверенных данных опасна: readObject() создаёт объекты любых сериализуемых классов из classpath, и подготовленный поток может привести к выполнению произвольного кода или исчерпанию памяти. Не принимайте Java-объекты извне (используйте JSON или Protobuf), а если без этого не обойтись — ограничьте классы фильтром ObjectInputFilter (JEP 290; для всей JVM — -Djdk.serialFilter) и проверяйте состояние в readObject().

Страница 1 из 1