Сериализация transient поля требования к сериализуемым классам

Обновлено: 01.07.2024

Что скрывает модификатор transient в Java - 1

Привет! В сегодняшней статье мы рассмотрим модификатор transient в Java. Поговорим о том, зачем данный модификатор нужен и как его правильно использовать. Поехали!

Вспомним сериализацию на практике

Что же, теперь рассмотрим сериализацию на практике. Если хочется получше разобраться в теме, советуем почитать материал Сериализация и десериализация в Java. Ну а в этой статье мы пройдемся по верхам и перейдем сразу к примерам. Предположим, у нас есть класс User с набором некоторых полей, геттерами и сеттерами, а также методом toString : Мы хотим сериализовывать объекты данного класса в дальнейшем. Напишем метод, который принимает объект User и строку path — путь до файла, в котором мы сохраним байты: Также напишем метод для десериализации. Метод принимает строку path (путь до файла из которого объект будет “загружен”) и возвращает объект типа User : Все инструменты готовы к работе. Пришло время расщеплять на атомы байты. Напишем метод main , в котором создадим объект класса User и сериализуем его. Затем загрузим и сравним с тем, что было изначально: Если запустить метод, мы увидим следующий вывод: Как видно из вывода, объекты идентичны. Но есть маленькое но… И это как раз то место, когда в игру вступает испанский стыд transient .

Алгоритм сериализации 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 . Обозначение нового объекта.
  • 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 : Число полей в классе.
  • 0x49 : Код типа поля. 49 это «I», которое закреплено за Int.
  • 00 07 : Длина имени поля.
  • 76 65 72 73 69 6F 6E: version , имя поля.
  • 0x74: TC_STRING . Обозначает новую строку.
  • 00 09 : Длина строки.
  • 4C 63 6F 6E 74 61 69 6E 3B: Lcontain; , Каноническое JVM обозначаение.
  • 0x78: TC_ENDBLOCKDATA , Конец опционального блока данных для объекта.
  • 0x72: TC_CLASSDESC . Обозначение нового класса.
  • 00 06 : Длина имени класса.
  • 70 61 72 65 6E 74: parent , имя класса
  • 0E DB D2 BD 85 EE 63 7A: SerialVersionUID , идентификатор класса.
  • 0x02 : Различные флаги. Этот флаг обозначает что класс поддерживает сериализацию.
  • 00 01 : Число полей в классе.
  • 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 , обозначает то что больше нет суперклассов, потому что мы достигли верха иерархии классов.
  • 00 00 00 0A: 10 , Значение parentVersion .
  • 00 00 00 42: 66 , Значение version .

* This source code was highlighted with Source Code Highlighter .

  • 0x73: TC_OBJECT , обозначает новый объект.
  • 0x72: TC_CLASSDESC , обозначает новый класс.
  • 00 07 : Длина имени класса.
  • 63 6F 6E 74 61 69 6E: contain , имя класса.
  • FC BB E6 0E FB CB 60 C7: SerialVersionUID , идентификатор этого класса.
  • 0x02 : Различные флаги. Этот флаг обозначает что класс поддерживает сериализацию.
  • 00 01 : Число полей в классе.
  • 0x49 : Код типа поля. 49 обозначает «I», которое закреплено за Int.
  • 00 0E : Длина имени поля.
  • 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion , имя поля.
  • 0x78: TC_ENDBLOCKDATA , конец опционального блока данных для объекта.
  • 0x70: TC_NULL
  • 00 00 00 0B: 11 , значение containVersion .

Когда использовать transient?

Пример с пользователем был нужен для того, чтобы погрузиться в контекст сериализации. Теперь поговорим предметнее о том, когда следует использовать модификатор transient .

В некоторых классах иногда бывают такие поля, которые вычисляются на основе других полей или же другой информации. Вычисляются, так сказать, на лету. Чтобы привести пример такого поля, представим себе заказ в интернет-магазине или же в каком-нибудь сервисе доставки еды. Каждый заказ, помимо прочей информации, состоит из списка товаров и итоговой стоимости. Она, в свою очередь, складывается из суммарной стоимости каждого товара. Выходит, что итоговую стоимость не стоит задавать “руками”: ее нужно вычислять программно, суммируя стоимость всех товаров. Подобные поля, которые следует вычислять программно, не нужно сериализовывать. Поэтому помечаем их модификатором transient .

Также бывают некоторые классы, которые хранят приватную информацию. Пример такого класса мы рассматривали в начале статьи. Не стоит допускать утечки такой информации за пределы JVM. Поэтому поля с подобными данными необходимо помечать модификатором transient , если вы собираетесь сериализовывать такой класс.

  • Поля, которые не реализуют интерфейс Serializable

Иногда класс содержит поля — объекты других классов, которые не реализуют интерфейс Serializable . Пример таких полей — логгеры, потоки ввода-вывода, объекты, которые хранят соединения с базой данных и прочие служебные классы. Если попытаться сериализовать объект, который содержит несериализуемые поля, возникнет ошибка java.io.NotSerializableException . Чтобы избежать этого, все поля, которые не реализуют интерфейс Serializable , необходимо помечать модификатором transient .

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

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

Как должен выглядеть сериализованный объект? Вспомните простой код из предыдущего раздела, который сериализует объект класса 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 .

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

transient и final

Рассмотрим такой пример. Модифицируем класс User из примера выше и добавим в него новое поле, которое будет константой c информацией о роли пользователя: А затем еще раз запустим метод main из первого примера: То мы увидим следующий вывод: Обратите внимание, что поле role , которое мы пометили модификатором transient , все равно подверглось сериализации. Дело в том, что Java сериализует все константы класса, независимо от того, есть или нет модификатор transient . При этом ни на этапе компиляции, ни в IDE вы не увидите ошибок. Эту маленькую деталь стоит просто держать в голове, где-то очень глубоко, и возможно, вы не удивитесь, когда столкнетесь с таким на практике.


Сериализация (Serialization) — это процесс, который переводит объект в последовательность байтов, по которой затем его можно полностью восстановить. Зачем это нужно? Дело в том, при обычном выполнении программы максимальный срок жизни любого объекта известен — от запуска программы до ее окончания. Сериализация позволяет расширить эти рамки и «дать жизнь» объекту так же между запусками программы.

Дополнительным бонусом ко всему является сохранение кроссплатформенности. Не важно какая у вас операционная система, сериализация переводит объект в поток байтов, который может быть восстановлен на любой ОС. Если вам необходимо передать объект по сети, вы можете сериализовать объект, сохранить его в файл и передать по сети получателю. Он сможет восстановить полученный объект. Так же сериализация позволяет осуществлять удаленный вызов методов (Java RMI), которые находятся на разных машинах с, возможно, разными операционными системами, и работать с ними так, словно они находятся на машине вызывающего java-процесса.

Реализовать механизм сериализации довольно просто. Необходимо, чтобы ваш класс реализовывал интерфейс Serializable. Это интерфейс — идентификатор, который не имеет методов, но он указывает jvm, что объекты этого класса могут быть сериализованы. Так как механизм сериализации связан с базовой системой ввода/вывода и переводит объект в поток байтов, для его выполнения необходимо создать выходной поток OutputStream, упаковать его в ObjectOutputStream и вызвать метод writeObject(). Для восстановления объекта нужно упаковать InputStream в ObjectInputStream и вызвать метод readObject().

В процессе сериализации вместе с сериализуемым объектом сохраняется его граф объектов. Т.е. все связанные с этим объекто, объекты других классов так же будут сериализованы вместе с ним.

Рассмотри пример сериализации объекта класса Person.


Вывод:


В данном примере класс Home создан для того чтобы продемонстрировать, что при сериализации объекта Person, с ним сериализуется и граф его объектов. Класс Home так же должен реализовывать интерфейс Serializable, иначе случится исключение java.io.NotSerializableException. Так же в примере описана сериализация с помощью класса ByteArrayOutputStream.

Из результатов выполнения программы можно сделать интересный вывод: при восстановлении объектов, у которых до сериализации была ссылка на один и тот же объект, этот объект будет восстановлен только один раз. Это видно по одинаковым ссылкам в объектах после восстановления:


Однако, так же видно, что при выполнении записи двумя потоками вывода (у нас это ObjectInputStream и ByteArrayOutputStream), объект home будет создан заново, несмотря на то, что он уже был создан до этого в одном из потоков. Мы видим это по разным адресам объектов home, полученных в двух потоках. Получается, что если выполнить сериализацию одним выходным поток, затем восстановить объект, то у нас есть гарантия восстановления полной сети объектов без лишних дубликатов. Конечно, в ходе выполнения программы состояние объектов может измениться, но это на совести программиста.

Проблема

Из примера так же видно, что при восстановлении объекта может возникнуть исключение ClassNotFoundException. С чем это связано? Дело в том, что мы легко можем сериализовать объект класса Person в файл, передать его по сети нашему товарищу, который может восстановить объект другим приложением, в котором класса Person попросту нет.

Своя сериализация. Как сделать?

Что делать, если вы хотите управлять сериализацией сами? Например, ваш объект хранит в себе логин и пароль пользователей. Вам необходимо сериализовать его для дальнейшей передачи его по сети. Передавать пароль в таком случае крайне ненадежно. Как решить эту задачу? Существует два способа. Первый, использовать ключевое слово transient. Второй, вместо реализации интереса Serializable использовать его расширение — интерфейс Externalizable. Рассмотрим примеры работы первого и второго способа для их сравнения.

Первый способ — Сериализация с использованием transient


Вывод:


Второй способ — Сериализация с реализацией интерфейса Externalizable


Вывод:


Первое отличие двух вариантов, которое бросается в глаза это размер кода. При реализации интерфейса Externalizable нам необходимо переопределить два метода: writeExternal() и readExternal(). В методе writeExternal() мы указываем какие поля будут сериализованы и как, в readExternal() как их прочитать. При использовании слова transient мы явно указываем, какое поле или поля не нужно сериализовывать. Так же заметим, что во втором способе мы явно создали конструктор по умолчанию, причем публичный. Зачем это сделано? Давайте попробуем запустить код без этого конструктора. И посмотрим на вывод:


Мы получили исключение java.io.InvalidClassException. С чем это связано? Если пройти по стек-трейсу можно выяснить, что в конструкторе класса ObjectStreamClass есть строчки:


Для интерфейса Externalizable будет вызван метод получения конструктора getExternalizableConstructor(), внутри которого мы через Reflection попробуем получить конструктор по умолчанию класса, для которого мы восстанавливаем объект. Если нам не удается его найти, или он не public, то мы получаем исключение. Обойти эту ситуацию можно следующим образом: не создавать явно никакого конструктора в классе и заполнять поля с помощью сеттеров и получать значение геттерами. Тогда при компиляции класса будет создан конструктор по умолчанию, который будет доступен для getExternalizableConstructor(). Для Serializable метод getSerializableConstructor() получает конструктор класса Object и от него ищет нужный класс, если не найдет, то получим исключение ClassNotFoundException. Выходит, что ключевое различие между Serializable и Externalizable в том, что первому не нужен конструктор для создания восстановления объекта. Он просто полностью восстановится из байтов. Для второго при восстановлении сначала будет создан объект с помощью конструктора в точке объявления, а затем в него будут записаны значения его полей из байтов, полученных при сериализации. Лично мне больше нравится первый способ, он гораздо проще. Причем, даже если нам нужно все таки задать поведение сериализации, мы можем не использовать Externalizable, а так же реализовать Serializable, добавив (не переопределив) в него методы writeObject() и readObject(). Но для того, чтобы они «работали» нужно точно соблюсти их сигнатуру.


Вывод:


Внутри наших добавленных методов вызываются defaultWriteObject() и defaultReadObject(). Они отвечают за сериализацию по умолчанию, как если бы она работала без добавленных нами методов.

На самом деле это только верхушка айсберга, если продолжить углубляться в механизм сериализации, то с высокой доли вероятности, можно отыскать еще нюансы, найдя которые мы скажем: «Сериализация… не все так просто».

Сериализация как она есть. Часть 1 - 2

Прежде всего, вопрос на засыпку. А сколько существует способов сделать объект сериализуемым? Практика показывает, что более 90% разработчиков отвечают на этот вопрос приблизительно одинаково (с точностью до формулировки) – такой способ один. Между тем, их два. Про второй вспоминают далеко не все, не говоря уж о том, чтобы сказать что-то внятное о его особенностях. Итак, каковы же эти способы? Про первый помнят все. Это уже упомянутая реализация java.io.Serializable , не требующая никаких усилий. Второй способ – это тоже реализация интерфейса, но уже другого: java.io.Externalizable . В отличие от java.io.Serializable , он содержит два метода, которые необходимо реализовать – writeExternal(ObjectOutput) и readExternal(ObjectInput) . В этих методах как раз и находится логика сериализации/десериализации. Замечание. В дальнейшем сериализацию с реализацией Serializable я буду иногда называть стандартной, а реализацию Externalizable – расширенной. Еще одно замечание . Я намеренно не затрагиваю сейчас такие возможности управления стандартной сериализацией, как определение readObject и writeObject , т.к. считаю эти способы в некоторой степени некорректными. Эти методы не определены в интерфейсе Serializable и являются, фактически, подпорками для обхода ограничений и придания стандартной сериализации гибкости. В Externalizable же методы, обеспечивающие гибкость, заложены изначально. Зададимся еще одним вопросом. А как, собственно, работает стандартная сериализация, с использованием java.io.Serializable ? А работает она через Reflection API. Т.е. класс разбирается как набор полей, каждое из которых пишется в выходной поток. Думаю, понятно, что операция эта неоптимальна по производительности. Насколько именно – выясним позднее. Между упомянутыми двумя способами сериализации существует еще одно серьезное отличие. А именно – в механизме десериализации. При использовании Serializable десериализация происходит так: под объект выделяется память, после чего его поля заполняются значениями из потока. Конструктор объекта при этом не вызывается. Тут надо еще отдельно рассмотреть такую ситуацию. Хорошо, наш класс сериализуемый. А его родитель? Совершенно необязательно! Более того, если наследовать класс от Object – родитель уж точно НЕсериализуемый. И пусть о полях Object мы ничего не знаем, но в наших собственных родительских классах они вполне могут быть. Что будет с ними? В поток сериализации они не попадут. Какие значения они примут при десериализации? Посмотрим на этот пример: Он прозрачен – у нас есть несериализуемый родительский класс и сериализуемый дочерний. И вот что получается: То есть при десериализации вызывается конструктор без параметров родительского НЕсериализуемого класса. И если такого конструктора не будет – при десериализации возникнет ошибка. Конструктор же дочернего объекта, того, который мы десериализуем, не вызывается, как и было сказано выше. Так ведут себя стандартные механизмы при использовании Serializable . При использовании же Externalizable ситуация иная. Сначала вызывается конструктор без параметров, а потом уже на созданном объекте вызывается метод readExternal, который и вычитывает, собственно, все свои данные. Потому – любой реализующий интерфейс Externalizable класс обязан иметь public конструктор без параметров! Более того, поскольку все наследники такого класса тоже будут считаться реализующими интерфейс Externalizable , у них тоже должен быть конструктор без параметров! Пойдем дальше. Существует такой модификатор поля как transient . Он означает, что это поле не должно быть сериализовано. Однако, как вы сами понимаете, указание это действует только на стандартный механизм сериализации. При использовании Externalizable никто не мешает сериализовать это поле, равно как и вычитать его. Если поле объявлено transient, то при десериализации объекта оно принимает значение по умолчанию. Еще один достаточно тонкий момент. При стандартной сериализации поля, имеющие модификатор static , не сериализуются. Соответственно, после десериализации это поле значения не меняет. Разумеется, при реализации Externalizable сериализовать и десериализовать это поле никто не мешает, однако я крайне не рекомендую этого делать, т.к. это может привести к трудноуловимым ошибкам. Поля с модификатором final сериализуются как и обычные. За одним исключением – их невозможно десериализовать при использовании Externalizable. Ибо final-поля должны быть инициализированы в конструкторе, а после этого в readExternal изменить значение этого поля будет невозможно. Соответственно – если вам необходимо сериализовать объект, имеющий final -поле, вам придется использовать только стандартную сериализацию. Еще один момент, который многие не знают. При стандартной сериализации учитывается порядок объявления полей в классе. Во всяком случае, так было в ранних версиях, в JVM версии 1.6 реализации Oracle уже порядок неважен, важны тип и имя поля. Состав же методов с очень большой вероятностью повлияет на стандартный механизм, при том, что поля могут вообще остаться теми же. Чтобы этого избежать, есть следующий механизм. В каждый класс, реализующий интерфейс Serializable , на стадии компиляции добавляется еще одно поле – private static final long serialVersionUID . Это поле содержит уникальный идентификатор версии сериализованного класса. Оно вычисляется по содержимому класса – полям, их порядку объявления, методам, их порядку объявления. Соответственно, при любом изменении в классе это поле поменяет свое значение. Это поле записывается в поток при сериализации класса. Кстати, это, пожалуй, единственный известный мне случай, когда static -поле сериализуется. При десериализации значение этого поля сравнивается с имеющимся у класса в виртуальной машине. Если значения не совпадают – инициируется исключение наподобие этого: Есть, однако, способ эту проверку если не обойти, то обмануть. Это может оказаться полезным, если набор полей класса и их порядок уже определен, а методы класса могут меняться. В этом случае сериализации ничего не угрожает, однако стандартный механизм не даст десериализовать данные с использованием байткода измененого класса. Но, как я уже сказал, его можно обмануть. А именно – вручную в классе определить поле private static final long serialVersionUID . В принципе, значение этого поля может быть абсолютно любым. Некоторые предпочитают ставить его равным дате модификации кода. Некоторые вообще используют 1L. Для получения стандартного значения (того, которое вычисляется внутренним механизмом) можно использовать утилиту serialver, входящую в поставку SDK. После такого определения значение поля будет фиксировано, следовательно, десериализация всегда будет разрешена. Более того, в версии 5.0 в документации появилось приблизительно следующее: крайне рекомендуется всем сериализуемым классам декларировать это поле в явном виде, ибо вычисление по умолчанию очень чувствительно к деталям структуры класса, которые могут различаться в зависимости от реализации компилятора, и вызывать таким образом неожиданные InvalidClassException при десериализации. Объявлять это поле лучше как private , т.к. оно относится исключительно к классу, в котором объявляется. Хотя в спецификации модификатор не оговорен. Рассмотрим теперь вот какой аспект. Пусть у нас есть такая структура классов: Иначе говоря, у нас есть класс, унаследованный от несериализуемого родителя. Можно ли сериализовать этот класс, и что для этого надо? Что будет с переменными родительского класса? Ответ такой. Да, сериализовать экземпляр класса B можно. Что для этого нужно? А нужно, чтобы у класса A был конструктор без параметров, public либо protected . Тогда при десериализации все переменные класса A будут инициализированы с помощью этого конструктора. Переменные класса B будут инициализированы значениями из потока сериализованных данных. Теоретически можно в классе B определить методы, о которых я говорил в начале – readObject и writeObject , – в начале которых производить (де-)сериализацию переменных класса B через in.defaultReadObject/out.defaultWriteObject , а потом – (де-)сериализацию доступных переменных из класса A (в нашем случае это iPublic , iProtected и iPackage , если B находится с том же пакете, что и A ). Однако, на мой взгляд, для этого лучше использовать расширенную сериализацию. Следующий момент, которого я хотел бы коснуться – сериализация нескольких объектов. Пусть у нас есть следующая структура классов: Что произойдет, если сериализовать экземпляр класса A ? Он потащит за собой экземпляр класса B , который, в свою очередь, потащит экземпляр C , который имеет ссылку на экземпляр A , тот же самый, с которого все начиналось. Замкнутый круг и бесконечная рекурсия? К счастью, нет. Посмотрим на следующий тестовый код: Что мы делаем? Мы создаем по экземпляру классов A , B и C , ставим им ссылки друг на друга, после чего сериализуем каждый из них. Потом мы десериализуем их обратно и проводим серию проверок. Что получится в результате: Итак, что можно извлечь из этого теста. Первое. Ссылки на объекты после десериализации отличаются от ссылок до нее. Иначе говоря, при сериализации/десериализации объект был скопирован. Этот метод используется иногда для клонирования объектов. Второй вывод, более сущеcтвенный. При сериализации/десериализации нескольких объектов, имеющих перекрестные ссылки, эти ссылки остаются действительными после десериализации. Иначе говоря, если до сериализации они указывали на один объект, то после десериализации они тоже будут указывать на один объект. Еще один небольшой тест в подтверждение этого: Объект класса B имеет ссылку на объект класса C . При сериализации b сериализуется вместе с экземпляром класса С , после чего тот же экземпляр c сериализуется трижды. Что получается после десериализации? Как видим, все четыре десериализованных объекта на самом деле представляют собой один объект – ссылки на него равны. Ровно как это и было до сериализации. Еще один интересный момент – что будет, если одновременно реализовать у класса Externalizable и Serializable ? Как в том вопросе – слон против кита – кто кого поборет? Поборет Externalizable . Механизм сериализации сначала проверяет его наличие, а уж потом – наличие Serializable Так что если класс B, реализующий Serializable , наследуется от класса A, реализующего Externalizable , поля класса B сериализованы не будут. Последний момент – наследование. При наследовании от класса, реализующего Serializable , никаких дополнительных действий предпринимать не надо. Сериализация будет распространяться и на дочерний класс. При наследовании от класса, реализующего Externalizable , необходимо переопределить методы родительского класса readExternal и writeExternal. Иначе поля дочернего класса сериализованы не будут. В этом случае надо бы не забыть вызвать родительские методы, иначе не сериализованы будут уже родительские поля. * * * С деталями, пожалуй, закончили. Однако есть один вопрос, который мы не затронули, глобального характера. А именно –

Зачем нужен Externalizable

Зачем вообще нужна расширенная сериализация? Ответ прост. Во-первых, она дает гораздо большую гибкость. Во-вторых, зачастую она может дать немалый выигрыш по объему сериализованных данных. В-третьих, существует такой аспект как производительность, о котором мы поговорим ниже. С гибкостью вроде как понятно всё. Действительно, мы можем управлять процессами сериализации и десериализации как хотим, что делает нас независимыми от любых изменений в классе (как я говорил чуть выше, изменения в классе способны сильно повлиять на десериализацию). Потому хочу сказать пару слов о выигрыше по объему. Допустим, у нас есть следующий класс: Остальное несущественно. Поля можно было бы сделать типа int, но это лишь усилило бы эффект примера. Хотя в реальности поля могут быть типа int по соображениям производительности. В любом случае, суть понятна. Класс представляет собой дату и время. Нам он интересен прежде всего с точки зрения сериализации. Возможно, проще всего было бы хранить простейший timestamp. Он имеет тип long, т.е. при сериализации он занял бы 8 байт. Кроме того, этот подход требует методов преобразования компонент в одно значение и обратно, т.е. – потеря в производительности. Плюс такого подхода – совершенно сумасшедшая дата, которая может поместиться в 64 бита. Это огромный запас прочности, чаще всего в реальности не нужный. Класс же, приведенный выше, займет 2 + 5*1 = 7 байт. Плюс служебные издержки на класс и 6 полей. Можно ли как-нибудь ужать эти данные? Наверняка. Секунды и минуты лежат в интервале 0-59, т.е. для их представления достаточно 6 бит вместо 8. Часы – 0-23 (5 бит), дни – 0-30 (5 бит), месяцы – 0-11 (4 бита). Итого, всё без учета года – 26 бит. До размера int еще остается 6 бит. Теоретически, в некоторых случаях этого может хватить для года. Если нет – добавление еще одного байта увеличивает размер поля данных до 14 бит, что дает промежуток 0-16383. Этого более чем достаточно в реальных приложениях. Итого – мы ужали размер данных, необходимых для хранения нужной информации, до 5 байт. Если не до 4. Недостаток тот же, что и в предыдущем случае – если хранить дату упакованной, то нужны методы преобразования. А хочется так – хранить в отдельных полях, а сериализовать в упакованном виде. Вот тут как раз целесообразно использовать Externalizable : Собственно, это все. После сериализации мы получаем служебные издержки на класс, два поля (вместо 6) и 5 байт данных. Что уже существенно лучше. Дальшейшую упаковку можно оставить специализированным библиотекам. Приведенный пример весьма прост. Его основное предназначение – показать, как можно применять расширенную сериализацию. Хотя возможный выигрыш в объеме сериализованных данных – далеко не основное преимущество, на мой взгляд. Основное же преимущество, помимо гибкости. (плавно переходим к следующему разделу. ) Ссылка на первоисточник: Сериализация как она есть

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


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

Рисунок 1.

Заключение

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

Об авторе

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

Заключение

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

Об авторе

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

В сегодняшнем мире типичное промышленное приложение будет иметь множество компонентов и будет распространено через различные системы и сети. В 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 gray">* This source code was highlighted with Source Code Highlighter .

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

Вспомним сериализацию

  1. Считать набор байтов из файла.
  2. Сконструировать из данного набора байтов исходный объект и задать каждому полю значение, которое было у объекта на момент сериализации.

Модификатор (ну наконец-таки) transient

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

Для начала следует убедиться, что класс сериализуемого объекта реализует интерфейс 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 gray">* This source code was highlighted with Source Code Highlighter .

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

Алгоритм сериализации 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 . Обозначение нового объекта.
  • 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 : Число полей в классе.
  • 0x49 : Код типа поля. 49 это «I», которое закреплено за Int.
  • 00 07 : Длина имени поля.
  • 76 65 72 73 69 6F 6E: version , имя поля.
  • 0x74: TC_STRING . Обозначает новую строку.
  • 00 09 : Длина строки.
  • 4C 63 6F 6E 74 61 69 6E 3B: Lcontain; , Каноническое JVM обозначаение.
  • 0x78: TC_ENDBLOCKDATA , Конец опционального блока данных для объекта.
  • 0x72: TC_CLASSDESC . Обозначение нового класса.
  • 00 06 : Длина имени класса.
  • 70 61 72 65 6E 74: parent , имя класса
  • 0E DB D2 BD 85 EE 63 7A: SerialVersionUID , идентификатор класса.
  • 0x02 : Различные флаги. Этот флаг обозначает что класс поддерживает сериализацию.
  • 00 01 : Число полей в классе.
  • 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 , обозначает то что больше нет суперклассов, потому что мы достигли верха иерархии классов.
  • 00 00 00 0A: 10 , Значение parentVersion .
  • 00 00 00 42: 66 , Значение version .

* This source code was highlighted with Source Code Highlighter .

  • 0x73: TC_OBJECT , обозначает новый объект.
  • 0x72: TC_CLASSDESC , обозначает новый класс.
  • 00 07 : Длина имени класса.
  • 63 6F 6E 74 61 69 6E: contain , имя класса.
  • FC BB E6 0E FB CB 60 C7: SerialVersionUID , идентификатор этого класса.
  • 0x02 : Различные флаги. Этот флаг обозначает что класс поддерживает сериализацию.
  • 00 01 : Число полей в классе.
  • 0x49 : Код типа поля. 49 обозначает «I», которое закреплено за Int.
  • 00 0E : Длина имени поля.
  • 63 6F 6E 74 61 69 6E 56 65 72 73 69 6F 6E: containVersion , имя поля.
  • 0x78: TC_ENDBLOCKDATA , конец опционального блока данных для объекта.
  • 0x70: TC_NULL
  • 00 00 00 0B: 11 , значение containVersion .

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

Как должен выглядеть сериализованный объект? Вспомните простой код из предыдущего раздела, который сериализует объект класса 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 .

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

Читайте также: