Открыть сервис

Java Serialization

Java Serialization — это механизм языка программирования Java, позволяющий преобразовывать объекты (а также примитивные типы данных) в последовательность байтов (байтовый поток) для последующего сохранения в файл, передачи по сети или хранения в базе данных. Обратный процесс — восстановление объекта из байтового потока — называется десериализацией. Механизм является встроенным и реализован на уровне виртуальной машины Java (JVM), что делает его простым в использовании, но накладывает ряд ограничений и требований безопасности.

История и развитие

Механизм сериализации появился в Java 1.1 (1997 год) как часть стандартной библиотеки (пакет java.io). Первоначально он был единственным стандартным способом сохранения состояния объектов. В последующих версиях Java механизм не претерпевал кардинальных изменений, но подвергался критике за низкую производительность и проблемы безопасности. В Java 9 (2017 год) была введена фильтрация сериализованных данных (JEP 290), позволяющая администраторам и разработчикам ограничивать классы, которые могут быть десериализованы. В Java 14 (2020 год) был добавлен механизм записей (Records), которые по умолчанию поддерживают сериализацию, но с упрощённой семантикой. В Java 17 (2021 год) вышло предупреждение о планах по удалению механизма сериализации в будущих версиях (JEP 411), так как он признан устаревшим и потенциально опасным.

Основные принципы работы

Требования к классам

Для того чтобы объект класса мог быть сериализован, класс должен реализовывать маркерный интерфейс java.io.Serializable. Этот интерфейс не содержит методов — он служит лишь сигналом JVM о том, что объект можно сериализовать. Если класс не реализует Serializable, при попытке сериализации будет выброшено исключение java.io.NotSerializableException.

Процесс сериализации

Сериализация выполняется с помощью класса ObjectOutputStream. Метод writeObject(Object obj) преобразует объект в поток байтов. В процессе сериализации JVM:

  1. Проверяет, реализует ли класс интерфейс Serializable.
  2. Записывает метаданные класса (имя, идентификатор версии serialVersionUID, описание полей).
  3. Рекурсивно записывает все поля объекта, включая поля родительских классов (если они также реализуют Serializable).
  4. Для каждого поля записывается его значение: примитивные типы — напрямую, ссылочные — рекурсивно.
  5. Если объект уже был сериализован ранее в том же потоке, вместо повторной записи сохраняется ссылка на ранее записанный объект (это позволяет избежать дублирования и циклических ссылок).

Процесс десериализации

Десериализация выполняется с помощью класса ObjectInputStream. Метод readObject() восстанавливает объект из байтового потока. В процессе десериализации JVM:

  1. Считывает метаданные класса.
  2. Определяет, доступен ли класс в текущей JVM (если класс отсутствует, выбрасывается ClassNotFoundException).
  3. Создаёт новый экземпляр объекта без вызова конструктора (используется специальный механизм JVM для создания объекта в «пустом» состоянии).
  4. Восстанавливает значения всех полей, включая final и transient (поля, помеченные transient, не сериализуются и при десериализации получают значения по умолчанию).
  5. Если класс содержит метод readObject(), он вызывается после восстановления полей (позволяет выполнить дополнительную инициализацию).

Ключевые элементы механизма

serialVersionUID

Каждый сериализуемый класс имеет уникальный идентификатор версии — serialVersionUID. Если он не объявлен явно, JVM вычисляет его автоматически на основе структуры класса (поля, методы, порядок объявления). Если при десериализации serialVersionUID считываемого потока не совпадает с serialVersionUID текущего класса, выбрасывается InvalidClassException. Рекомендуется явно объявлять serialVersionUID как статическое финальное поле типа long, чтобы обеспечить совместимость при изменениях класса.

Ключевое слово transient

Поля, помеченные модификатором transient, не участвуют в сериализации. При десериализации они получают значения по умолчанию (0 для чисел, null для ссылок, false для boolean). Это используется для полей, которые не имеют смысла после восстановления (например, временные кэши, соединения с базой данных, пароли).

Кастомная сериализация

Разработчик может изменить поведение сериализации, определив в классе методы:

  • private void writeObject(ObjectOutputStream out) throws IOException — вызывается во время сериализации.
  • private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException — вызывается во время десериализации.
  • private void readObjectNoData() throws ObjectStreamException — вызывается, если в потоке нет данных для данного класса (например, при десериализации объекта более новой версии класса).
  • Object writeReplace() throws ObjectStreamException — позволяет заменить сериализуемый объект другим объектом.
  • Object readResolve() throws ObjectStreamException — позволяет заменить десериализованный объект другим объектом (часто используется для реализации синглтонов).

Пример использования

```java import java.io.*;

public class Person implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; private transient String password; // не сериализуется

public Person(String name, int age, String password) { this.name = name; this.age = age; this.password = password; }

@Override public String toString() { return "Person{name='" + name + "', age=" + age + ", password='" + password + "'}"; }

public static void main(String[] args) throws IOException, ClassNotFoundException { Person original = new Person("Иван", 30, "secret123");

// Сериализация try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.ser"))) { oos.writeObject(original); }

// Десериализация try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) { Person restored = (Person) ois.readObject(); System.out.println(restored); // password будет null } } } ```

Особенности и ограничения

Производительность

Стандартная сериализация Java использует рефлексию и рекурсивный обход графа объектов, что делает её медленной по сравнению с современными альтернативами (например, Protocol Buffers, JSON, Kryo). Для больших графов объектов сериализация может занимать значительное время и потреблять много памяти.

Безопасность

Десериализация недоверенных данных является одной из самых опасных уязвимостей в Java. Злоумышленник может создать специально сформированный поток байтов, который при десериализации вызовет выполнение произвольного кода (например, через цепочки вызовов методов из стандартной библиотеки). Для защиты рекомендуется:

  • Использовать фильтрацию (JEP 290) — ObjectInputFilter.
  • Не десериализовать данные из непроверенных источников.
  • Использовать альтернативные форматы сериализации (JSON, XML, Protocol Buffers).

Совместимость версий

Изменение структуры класса (добавление, удаление или переименование полей) может нарушить совместимость с ранее сериализованными данными. Для обеспечения обратной совместимости рекомендуется:

  • Явно объявлять serialVersionUID.
  • Использовать transient для полей, которые могут быть добавлены в будущем.
  • Реализовать кастомные методы readObject() и writeObject() для обработки изменений.

Альтернативы

В современной разработке стандартная сериализация Java часто заменяется более безопасными и производительными решениями:

  • JSON (библиотеки Jackson, Gson) — человекочитаемый формат, не зависит от версии Java.
  • XML (JAXB) — поддерживает сложные схемы данных.
  • Protocol Buffers (Google) — бинарный формат, высокая скорость и компактность.
  • Kryo — высокопроизводительная сериализация для Java.
  • YAML — человекочитаемый формат, часто используется для конфигураций.

Критика

Механизм Java Serialization подвергается критике по нескольким причинам:

  • Сложность и хрупкость — зависимость от версии класса, проблемы с циклическими ссылками, неочевидное поведение с наследованием.
  • Низкая производительность — рефлексия и рекурсия делают его медленным.
  • Проблемы безопасности — десериализация является вектором атак, что привело к многочисленным уязвимостям в реальных проектах (например, в Apache Commons Collections, JBoss).
  • Отсутствие контроля над форматом — разработчик не может легко изменить формат данных без написания кастомного кода.

В 2021 году компания Oracle объявила о планах по удалению механизма сериализации из будущих версий Java (JEP 411), что стимулирует переход на альтернативные решения.

Источники

  • Java Language Specification, Chapter 11: Serialization
  • Oracle Java Documentation: «Java Object Serialization Specification»
  • JEP 290: Filter Incoming Serialization Data
  • JEP 411: Deprecate the Serialization Mechanism for Removal
  • Bloch, Joshua. «Effective Java», 3rd Edition, Item 85-90 (о сериализации и безопасности)

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →