1. Главная страница » Компьютеры » Java serializable что это

Java serializable что это

Автор: | 16.12.2019

Содержание

Сериализация объекта представляет процесс перевода какой-либо структуры данных в последовательность битов. Обратной к операции сериализации является операция десериализации, т.е. восстановление начального состояния структуры данных из битовой последовательности. Существует два способа сериализации объекта : стандартная сериализация java.io.Serializable и «расширенная» сериализация java.io.Externalizable.

Интерфейс java.io.Serializable

При использовании Serializable применяется стандартный алгоритм сериализации, который с помощью рефлексии (Reflection API) выполняет

  • запись в поток метаданных о классе, ассоциированном с объектом (имя класса, идентификатор SerialVersionUID, идентификаторы полей класса),
  • рекурсивную запись в поток описания суперклассов до класса java.lang.Object (не включительно),
  • запись примитивных значений полей сериализуемого экземпляра, начиная с полей самого верхнего суперкласса,
  • рекурсивную запись объектов, которые являются полями сериализуемого объекта.

При этом ранее сериализованные объекты повторно не сериализуются, что позволяет алгоритму корректно работать с циклическими ссылками.

Для выполнения десериализации под объект выделяется память, после чего его поля заполняются значениями из потока. Конструктор объекта при этом не вызывается. Однако при десериализации будет вызван конструктор без параметров родительского несериализуемого класса, а его отсутствие повлечёт ошибку десериализации.

Интерфейс java.io.Externalizable

При реализации интерфейса Externalizable вызывается пользовательская логика сериализации. Способ сериализации и десериализации описывается в методах writeExternal и readExternal. Во время десериализации вызывается конструктор без параметров, а потом уже на созданном объекте вызывается метод readExternal.

Расширенный алгоритм сериализации может быть использован, если данные содержат «конфиденциальную» информацию. В этом случае имеет смысл шифровать сериализуемые данные и дешифровать их при десерилизации, что требует реализацию собственного алгоритма.

Где используется сериализация Serializable?

Сериализация была введена в JDK 1.1 и позволяет преобразовать отдельный объект или группу объектов в поток битов для передачи по сети или для сохранения в файл. И как было сказано выше, данный массив байтов или поток битов, можно обратно преобразовать в объекты Java. Главным образом это происходит автоматически благодаря классам ObjectInputStream и ObjectOutputStream.

Класс сериализации Person

В следующем листинге показан класс Person, реализующий интерфейс Serializable.

Далее класс Person будет использоваться в примерах, чтобы показать дополнительные возможности, связанные с сериализацией Java-объектов.

Модификатор поля transient

Использование при описании поля класса модификатора transient позволяет исключить указанное поле из сериализации. Это бывает полезно для секретных (пароль) или не особо важных данных. Если, например, при описании объекта Person включить следующее поле address

то в результате сериализации и десериализации адрес объекта принимает значение по умолчанию или будет null.

Модификатор transient действует только на стандартный механизм сериализации Serializable. При использовании Externalizable никто не мешает сериализовать это поле, равно как и использовать его для определения других полей.

Модификатор поля static

При стандартной сериализации поля, имеющие модификатор static, не сериализуются. Соответственно, после десериализации это поле значения не меняет. При использовании реализации Externalizable сериализовать и десериализовать статическое поле можно, но не рекомендуется этого делать, т.к. это может сопровождаться трудноуловимыми ошибками.

Модификатор поля final

Поля с модификатором final сериализуются как и обычные. За одним исключением – их невозможно десериализовать при использовании Externalizable, поскольку final-поля должны быть инициализированы в конструкторе, а после этого в readExternal изменить значение этого поля будет невозможно. Соответственно, если необходимо сериализовать объект с final-полем неоходимо использовать только стандартную сериализацию.

Пример тестирования сериализации и десериализации

Для тестирования сериализации и десериализации объекта Person будем использовать юнит-тест JUnit, в котором создадим 2 объекта, запишем объекты в файл, после чего восстановим их. Более подробно об использовании JUnit сказано на странице Тестирование программы.

В примере ничего нового или удивительного не представлено – это основы сериализации, которые желательно знать, особенно при разработке WEB-приложений.

Переопределение сериализации в интерфейсе Serializable

Если у сериализуемого объекта реализован один из следующих методов, то механизм сериализации будет использовать его, а не метод по умолчанию :

  • writeObject — запись объекта в поток;
  • readObject — чтение объекта из потока;
  • writeReplace — позволяет заменить себя экземпляром другого класса перед записью;
  • readResolve — позволяет заменить на себя другой объект после чтения.

Ниже приводятся примеры использования данных методов.

Сериализация не безопасна

Двоичный формат сериализации полностью документирован и обратим. Для того, чтобы определить параметры объекта и его значения, достаточно просто вывести содержимое сериализованного потока в консоль. Это имеет некоторые связанные с безопасностью неприятные последствия. Например, при выполнении удаленного вызова метода с помощью RMI все закрытые поля пересылаемых по сети объектов выглядят в потоке сокета почти как обычный текст, что, конечно же, нарушает даже самые простые правила безопасности.

К счастью разработчиков Java Serialization API позволяет "вклиниться" в процесс сериализации, и изменить (или запутать) поля данных как перед сериализацией, так и после десериализации. Это можно сделать, определив методы writeObject, readObject объекта Serializable.

Изменение сериализованных данных, writeObject, readObject

Чтобы изменить процесс сериализации в классе Person реализуем метод writeObject. Для модифицирования процесса десериализации определим в том же классе метод readObject. При реализации этих методов необходимо корректно восстановить данные.

В данных методах значение возраста age просто умножается на 4 при записи объекта, а при восстановлении — делится на четыре. Алгоритм, конечно же можно усложнить; в примере он представлен только для демонстрации возможностей сериализации.

Сериализация и рефакторинг кода

Сериализация позволяет вносить небольшие изменения в структуру класса, так что даже после рефакторинга класс ObjectInputStream по-прежнему будет с ним прекрасно работать. К наиболее важным изменениям, с которыми спецификация Java Object Serialization может справляться автоматически:

  • добавление новых полей в класс;
  • изменение полей из статических в нестатические;
  • изменение полей из транзитных в нетранзитные.
Читайте также:  Eset хороший ли антивирус

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

Рефакторинг сериализованного класса

Для тестирования сериализации с измененной структурой класса Person на основе предыдущего примера необходимо проделать следующие предварительные шаги :

    Создать файл данных с использованием метода setUpBefore >

Теперь можно выполнить тест testSerialization класса JUnitPerson и увидеть, что тест прошел успешно, т.е. класс ObjectInputStream прочитал данные и объект был восстановлен корректно. При желании можно в конце кода testSerialization вставить строку

чтобы убедиться, что значения объекта восстановлены правильно, и что параметр gender равен null.

Проверка десериализованного объекта, ObjectInputValidation и validateObject

Если есть необходимость выполнения контроля за значениями десериализованного/восстановленного объекта, то можно использовать интерфейс ObjectInputValidation с переопределением метода validateObject. В следующем листинге представлены изменения, которые следует внести в описание класса Person, чтобы контролировать возраст.

Если вызвать метод validateObject после десериализации объекта, то будет вызвано исключение InvalidObjectException при значении возраста за пределами 39. 60.

Подписывание сериализованных данных

Чтобы убедиться, что данные не были изменены в файле или при пересылке по сети их можно «подписать». Несмотря на то, что управление подписями реализовать можно и с помощью методов writeObject и readObject, для этого есть более подходящий способ.

Если требуется зашифровать и подписать объект, то проще всего поместить его в оберточный класс javax.crypto.SealedObject и/или java.security.SignedObject. Данные классы являются сериализуемыми, поэтому при оборачивании объекта в SealedObject создается подобие "подарочной упаковки" вокруг исходного объекта. Для шифрования необходимо создать симметричный ключ, управление которым должно осуществляться отдельно. Аналогично, для проверки данных можно использовать класс SignedObject, для работы с которым также нужен симметричный ключ, управляемый отдельно. Эти два объекта позволяют упаковывать и подписывать сериализованные данные, не отвлекаясь на детали проверки и шифрования цифровых подписей.

Листинг теста подписи объекта

Прокси-класс для сериализации

Иногда класс может включать элемент, который позволяет получить значения отдельных полей класса по определенному алгоритму. В этих случаях необязательно сериализовывать весь объект. Можно было бы пометить восстанавливаемые поля как «транзитные». Однако в классе всё равно требуется явно указывать код (определять метод), который при обращении к полю каждый раз проверял бы его инициализацию. Для этих целей лучше использовать специальный прокси-класс, из которого можно восстановить объект.

Листинг прокси-класса

В прокси-классе определим метод readResolve, которой будет вызываться во время десериализации объекта, чтобы вернуть объект-замену. Конструктор прокси-класса будет упаковывать объект PersonY во внутреннее поле data.

Класс PersonY создадим на основе базового класса Person с добавлением метода writeReplace следующего вида :

Вместе методы writeReplace и readResolve позволяют классу PersonY упаковывать все данные (или их наиболее важную часть) в объект класса PersonProxy, помещать его в поток и затем распаковать его при десериализации.

Пример тестирования прокси-класса JUnitPersonProxy

Особенности сериализации Date

Если объект сериализации включает поля типа Date, то здесь следует, при необходимости, учитывать временной сдвиг между различными регионами. В противном случае может возникнуть ситуация, при которой на сервере будет создан объект Date с одним временем в определенной TimeZone, а на клиенте он будет десериализован с другой TimeZone. В зависимости от времени создания игнорирование TimeZone может привести к изменению даты. Решение данной проблемы представлено на странице описания TimeZone.

Использование сериализации

Технология RMI (Java Remote Method Invocation), построенная на сериализации, позволяет java-приложению, запущенному на одной виртуальной машине, вызвать методы объекта, работающего на другой виртуальной машине JVM (Java Virtual Machine).

Скачать пример Serialization

Исходный код рассмотренного примера Serialization в виде проекта Eclipse можно скачать здесь (15.0 Kб).

Зачем сериализация нужна?

В сегодняшнем мире типичное промышленное приложение будет иметь множество компонентов и будет распространено через различные системы и сети. В Java всё представлено в виде объектов; Если двум компонентам Java необходимо общаться друг с другом, то им необходим механизм для обмена данными. Есть несколько способов реализовать этот механизм. Первый способ это разработать собственный протокол и передать объект. Это означает, что получатель должен знать протокол, используемый отправителем для воссоздания объекта, что усложняет разработку сторонних компонентов. Следовательно, должен быть универсальный и эффективный протокол передачи объектов между компонентами. Сериализация создана для этого, и компоненты Java используют этот протокол для передачи объектов.

Рисунок 1 демонстрирует высоко-уровневое представление клиент-серверной коммуникации, где объект передаётся с клиента на сервер посредством сериализации.

Рисунок 1.

Как сериализовать объект?

Для начала следует убедиться, что класс сериализуемого объекта реализует интерфейс java.io.Serializable как показано в листинге 1.

class TestSerial implements Serializable <
public byte version = 100;
public byte count = 0;
>

* This source code was highlighted with Source Code Highlighter .

В Листинге 1 только одна вещь отличается от создания нормального класса, это реализация интерфейса java.io.Serializable . Интерфейс Serializable это интерфейс-маркер; в нём не задекларировано ни одного метода. Но говорит сериализующему механизму, что класс может быть сериализован.

Теперь у нас есть всё необходимое для сериализации объекта, следующим шагом будет фактическая сериализация объекта. Она делается вызовом метода writeObject() класса java.io.ObjectOutputStream , как показано в листинге 2.

public static void main( String args[]) throws IOException <
FileOutputStream fos = new FileOutputStream( "temp.out" );
ObjectOutputStream oos = new ObjectOutputStream(fos);
TestSerial ts = new TestSerial();
oos.writeObject(ts);
oos.flush();
oos.close();
>

* This source code was highlighted with Source Code Highlighter .

В листинге 2 показано сохранение состояния экземпляра TestSerial в файл с именем temp.out

Для воссоздания объекта из файла, необходимо применить код из листинга 3.

public static void main( String args[]) throws IOException <
FileInputStream fis = new FileInputStream( "temp.out" );
ObjectInputStream oin = new ObjectInputStream(fis);
TestSerial ts = (TestSerial) oin.readObject();
System. out .println( "version=" +ts.version);
>

* This source code was highlighted with Source Code Highlighter .

Восстановление объекта происходит с помощью вызова метода oin.readObject() . В методе происходит чтение набора байт из файла и создаие точной копии графа оригинального объекта. oin.readObject() может прочитать любой сериализованный объект, поэтому необходимо полученный объект приводить к конкретному типу.
Выполненный код выведет version=100 в стандартный вывод.

Формат сериализованного объекта

Как должен выглядеть сериализованный объект? Вспомните простой код из предыдущего раздела, который сериализует объект класса TestSerial и записывает в temp.out . В листинге 4 показано содержимое файла temp.out , в шестнадцатеричном виде.

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 A0 0C 34 00 FE B1 DD F9 02 00 02 42 00 05
63 6F 75 6E 74 42 00 07 76 65 72 73 69 6F 6E 78
70 00 64

Если вы снова посмотрите на TestSerial , то увидите, что у него всего 2 байтовых члена. Как показано в листинге 5.

public byte version = 100;
public byte count = 0;

* This source code was highlighted with Source Code Highlighter .

Читайте также:  Panasonic lumix dc gx800 kit

Размер байтовой переменной один байт, и следовательно полный размер объекта (без заголовка) — два байта. Но размер сериализованного объекта 51 байт. Удивлены? Откуда взялись эти дополнительные байты и что они обозначают? Они добавлены сериализующим алгоритмом и необходимы для воссоздания объекта. В следующем абзаце будет подробно описан этот алгоритм.

Алгоритм сериализации Java

К этому моменту у вас уже должно быть достаточно знаний, чтобы сериализовать объект. Но как работает этот механизм? Алгоритм сериализации делает следующие вещи:

  • запись метаданных о классе ассоциированном с объектом
  • рекурсивная запись описания суперклассов, до тех пор пока не будет достигнут java.lang.object
  • после окончания записи метаданных начинается запись фактических данных ассоциированных с экземпляром, только в этот раз начинается запись с самого верхнего суперкласса
  • рекурсивная запись данных ассоциированных с экземпляром начиная с самого низшего суперкласса

В листинге 6 указан пример охватывающий все возможные случаи сериализации

class parent implements Serializable <
int parentVersion = 10;
>

class contain implements Serializable <
int containVersion = 11;
>
public class SerialTest extends parent implements Serializable <
int version = 66;
contain con = new contain();

public int getVersion() <
return version;
>
public static void main( String args[]) throws IOException <
FileOutputStream fos = new FileOutputStream( "temp.out" );
ObjectOutputStream oos = new ObjectOutputStream(fos);
SerialTest st = new SerialTest();
oos.writeObject(st);
oos.flush();
oos.close();
>
>

* This source code was highlighted with Source Code Highlighter .

В примере сериализуется объект класса SerialTest , который наследуется от parent и содержит объект-контейнер класса contain . В листинге 7 показан сериализованный объект.

AC ED 00 05 73 72 00 0A 53 65 72 69 61 6C 54 65
73 74 05 52 81 5A AC 66 02 F6 02 00 02 49 00 07
76 65 72 73 69 6F 6E 4C 00 03 63 6F 6E 74 00 09
4C 63 6F 6E 74 61 69 6E 3B 78 72 00 06 70 61 72
65 6E 74 0E DB D2 BD 85 EE 63 7A 02 00 01 49 00
0D 70 61 72 65 6E 74 56 65 72 73 69 6F 6E 78 70
00 00 00 0A 00 00 00 42 73 72 00 07 63 6F 6E 74
61 69 6E FC BB E6 0E FB CB 60 C7 02 00 01 49 00
0E 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E 78
70 00 00 00 0B

На рисунке 2 показан сценарий алгоритма сериализации.

Рисунок 2.

Давайте рассмотрим, что собой представляет каждый байт в сериализованном объекте. В начале идёт информация о протоколе сериализации:

  • AC ED: STREAM_MAGIC . Говорит о том, что используется протокол сериазизации.
  • 00 05: STREAM_VERSION . Версия сериализации.
  • 0x73: TC_OBJECT . Обозначение нового объекта.

На первом шаге алгоритм сериализации записывает описание класса ассоциированного с объектом. В примере был сериализован объект класса SerialTest , следовательно алгоритм начал записывать описание класса SerailTest .

  • 0x72: TC_CLASSDESC . Обозначение нового класса.
  • 00 0A : Длина имени класса.
  • 53 65 72 69 61 6c 54 65 73 74: SerialTest , имя класса.
  • 05 52 81 5A AC 66 02 F6: SerialVersionUID , идентификатор класса.
  • 0x02 : Различные флаги. Этот специфический флаг говорит о том, что объект поддерживает сериализацию.
  • 00 02 : Число полей в классе.

Теперь алгоритм записывает поле int version = 66; .

  • 0x49 : Код типа поля. 49 это «I», которое закреплено за Int.
  • 00 07 : Длина имени поля.
  • 76 65 72 73 69 6F 6E: version , имя поля.

Затем алгоритм записывает следующее поле, contain con = new contain(); . Это объект следовательно будет записано каноническое JVM обозначение этого поля.

  • 0x74: TC_STRING . Обозначает новую строку.
  • 00 09 : Длина строки.
  • 4C 63 6F 6E 74 61 69 6E 3B: Lcontain; , Каноническое JVM обозначаение.
  • 0x78: TC_ENDBLOCKDATA , Конец опционального блока данных для объекта.

Следующим шагом алгоритма является запись описания класса parent , который является непосредственным суперклассом для SerialTest .

  • 0x72: TC_CLASSDESC . Обозначение нового класса.
  • 00 06 : Длина имени класса.
  • 70 61 72 65 6E 74: parent , имя класса
  • 0E DB D2 BD 85 EE 63 7A: SerialVersionUID , идентификатор класса.
  • 0x02 : Различные флаги. Этот флаг обозначает что класс поддерживает сериализацию.
  • 00 01 : Число полей в классе.

Теперь алгоритм записывает описание полей класса parent , класс имеет одно поле, int parentVersion = 100; .

  • 0x49 : Код типа поля. 49 обозначает «I», которое закреплено за Int.
  • 00 0D : Длина имени поля.
  • 70 61 72 65 6E 74 56 65 72 73 69 6F 6E : parentVersion, имя поля.
  • 0x78: TC_ENDBLOCKDATA , конец опционального блока данных для объекта.
  • 0x70: TC_NULL , обозначает то что больше нет суперклассов, потому что мы достигли верха иерархии классов.

До этого алгоритм сериализации записывал описание классов ассоциированных с объектом и всех его суперклассов. Теперь будут записаны фактические данные ассоциированные с объектом. Сначала записываются члены класса parent :

  • 00 00 00 0A: 10 , Значение parentVersion .

Дальше перемещаемся в SerialTest

  • 00 00 00 42: 66 , Значение version .

Следующие несколько байт очень интересны. Алгоритму необходимо записать информацию объекте класса contain .

* This source code was highlighted with Source Code Highlighter .

Заключение

В этой статье вы увидели как сериализовать объект, и узнали как работает алгоритм сериализации. Я надеюсь эта статья помогла вам лучше понять что происходит, когда вы сериализуете объект.

Об авторе

Sathiskumar Palaniappan имеет более чем 4-х летний опыт работы в IT-индестрии, и работает с Java технологиями более 3 лет. На данный момент он работает system software engineer в Java Technology Center, IBM Labs. Также имеет опыт работы в телекоммуникационной индустрии.

Читайте также:  Logitech g410 atlas spectrum

To allow subtypes of non-serializable classes to be serialized, the subtype may assume responsibility for saving and restoring the state of the supertype’s public, protected, and (if accessible) package fields. The subtype may assume this responsibility only if the class it extends has an accessible no-arg constructor to initialize the class’s state. It is an error to declare a class Serializable if this is not the case. The error will be detected at runtime.

During deserialization, the fields of non-serializable classes will be initialized using the public or protected no-arg constructor of the class. A no-arg constructor must be accessible to the subclass that is serializable. The fields of serializable subclasses will be restored from the stream.

When traversing a graph, an object may be encountered that does not support the Serializable interface. In this case the NotSerializableException will be thrown and will identify the class of the non-serializable object.

Classes that require special handling during the serialization and deserialization process must implement special methods with these exact signatures:

The writeObject method is responsible for writing the state of the object for its particular class so that the corresponding readObject method can restore it. The default mechanism for saving the Object’s fields can be invoked by calling out.defaultWriteObject. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.

The readObject method is responsible for reading from the stream and restoring the classes fields. It may call in.defaultReadObject to invoke the default mechanism for restoring the object’s non-static and non-transient fields. The defaultReadObject method uses information in the stream to assign the fields of the object saved in the stream with the correspondingly named fields in the current object. This handles the case when the class has evolved to add new fields. The method does not need to concern itself with the state belonging to its superclasses or subclasses. State is saved by writing the individual fields to the ObjectOutputStream using the writeObject method or by using the methods for primitive data types supported by DataOutput.

The readObjectNoData method is responsible for initializing the state of the object for its particular class in the event that the serialization stream does not list the given class as a superclass of the object being deserialized. This may occur in cases where the receiving party uses a different version of the deserialized instance’s class than the sending party, and the receiver’s version extends classes that are not extended by the sender’s version. This may also occur if the serialization stream has been tampered; hence, readObjectNoData is useful for initializing deserialized objects properly despite a "hostile" or incomplete source stream.

Serializable classes that need to designate an alternative object to be used when writing an object to the stream should implement this special method with the exact signature:

This writeReplace method is invoked by serialization if the method exists and it would be accessible from a method defined within the class of the object being serialized. Thus, the method can have private, protected and package-private access. Subclass access to this method follows java accessibility rules.

Classes that need to designate a replacement when an instance of it is read from the stream should implement this special method with the exact signature.

This readResolve method follows the same invocation rules and accessibility rules as writeReplace.

The serialization runtime associates with each serializable class a version number, called a serialVersionUID, which is used during deserialization to verify that the sender and receiver of a serialized object have loaded classes for that object that are compatible with respect to serialization. If the receiver has loaded a class for the object that has a different serialVersionUID than that of the corresponding sender’s class, then deserialization will result in an InvalidClassException . A serializable class can declare its own serialVersionUID explicitly by declaring a field named "serialVersionUID" that must be static, final, and of type long :

If a serializable class does not explicitly declare a serialVersionUID, then the serialization runtime will calculate a default serialVersionUID value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification. However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, and can thus result in unexpected InvalidClassException s during deserialization. Therefore, to guarantee a consistent serialVersionUID value across different java compiler implementations, a serializable class must declare an explicit serialVersionUID value. It is also strongly advised that explicit serialVersionUID declarations use the private modifier where possible, since such declarations apply only to the immediately declaring class—serialVersionUID fields are not useful as inherited members. Array classes cannot declare an explicit serialVersionUID, so they always have the default computed value, but the requirement for matching serialVersionUID values is waived for array classes.

  • Overview
  • Package
  • >Java™ Platform
    Standard Ed. 7
  • Summary:
  • Nested |
  • Field |
  • Constr |
  • Method
  • Detail:
  • Field |
  • Constr |
  • Method

Submit a bug or feature
For further API reference and developer documentation, see Java SE Documentation. That documentation contains more detailed, developer-targeted descriptions, with conceptual overviews, definitions of terms, workarounds, and working code examples.
Copyright © 1993, 2018, Oracle and/or its affiliates. All rights reserved. Use is subject to license terms. Also see the documentation redistribution policy.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *