Сериализация в Java. Не все так просто
Сериализация (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(). Они отвечают за сериализацию по умолчанию, как если бы она работала без добавленных нами методов.
На самом деле это только верхушка айсберга, если продолжить углубляться в механизм сериализации, то с высокой доли вероятности, можно отыскать еще нюансы, найдя которые мы скажем: «Сериализация… не все так просто».
Сериализация объектов
Сериализация объекта представляет процесс перевода какой-либо структуры данных в последовательность битов. Обратной к операции сериализации является операция десериализации, т.е. восстановление начального состояния структуры данных из битовой последовательности. Существует два способа сериализации объекта : стандартная сериализация java.io.Serializable и «расширенная» сериализация java.io.Externalizable.
Интерфейс java.io.Serializable
При использовании Serializable применяется стандартный алгоритм сериализации, который с помощью рефлексии (Reflection API) выполняет
При этом ранее сериализованные объекты повторно не сериализуются, что позволяет алгоритму корректно работать с циклическими ссылками.
Для выполнения десериализации под объект выделяется память, после чего его поля заполняются значениями из потока. Конструктор объекта при этом не вызывается. Однако при десериализации будет вызван конструктор без параметров родительского несериализуемого класса, а его отсутствие повлечёт ошибку десериализации.
Интерфейс 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
Если у сериализуемого объекта реализован один из следующих методов, то механизм сериализации будет использовать его, а не метод по умолчанию :
Ниже приводятся примеры использования данных методов.
Сериализация не безопасна
Двоичный формат сериализации полностью документирован и обратим. Для того, чтобы определить параметры объекта и его значения, достаточно просто вывести содержимое сериализованного потока в консоль. Это имеет некоторые связанные с безопасностью неприятные последствия. Например, при выполнении удаленного вызова метода с помощью RMI все закрытые поля пересылаемых по сети объектов выглядят в потоке сокета почти как обычный текст, что, конечно же, нарушает даже самые простые правила безопасности.
К счастью разработчиков Java Serialization API позволяет «вклиниться» в процесс сериализации, и изменить (или запутать) поля данных как перед сериализацией, так и после десериализации. Это можно сделать, определив методы writeObject, readObject объекта Serializable.
Изменение сериализованных данных, writeObject, readObject
Чтобы изменить процесс сериализации в классе Person реализуем метод writeObject. Для модифицирования процесса десериализации определим в том же классе метод readObject. При реализации этих методов необходимо корректно восстановить данные.
Сериализация и рефакторинг кода
Сериализация позволяет вносить небольшие изменения в структуру класса, так что даже после рефакторинга класс ObjectInputStream по-прежнему будет с ним прекрасно работать. К наиболее важным изменениям, с которыми спецификация Java Object Serialization может справляться автоматически:
Обратные изменения (нестатических и нетранзитных полей в статические и транзитные) или удаление полей требуют определенной дополнительной обработки в зависимости от того, какая степень обратной совместимости требуется.
Рефакторинг сериализованного класса
Для тестирования сериализации с измененной структурой класса Person на основе предыдущего примера необходимо проделать следующие предварительные шаги :
Листинг изменений класса Person
Теперь можно выполнить тест 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 необходимо общаться друг с другом, то им необходим механизм для обмена данными. Есть несколько способов реализовать этот механизм. Первый способ это разработать собственный протокол и передать объект. Это означает, что получатель должен знать протокол, используемый отправителем для воссоздания объекта, что усложняет разработку сторонних компонентов. Следовательно, должен быть универсальный и эффективный протокол передачи объектов между компонентами. Сериализация создана для этого, и компоненты Java используют этот протокол для передачи объектов.
Рисунок 1 демонстрирует высоко-уровневое представление клиент-серверной коммуникации, где объект передаётся с клиента на сервер посредством сериализации. 
Рисунок 1.
Как сериализовать объект?
Для начала следует убедиться, что класс сериализуемого объекта реализует интерфейс java.io.Serializable как показано в листинге 1.
class TestSerial implements Serializable <
public byte version = 100;
public byte count = 0;
>
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();
>
В листинге 2 показано сохранение состояния экземпляра TestSerial в файл с именем temp.out
Для воссоздания объекта из файла, необходимо применить код из листинга 3.
Формат сериализованного объекта
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
public byte version = 100;
public byte count = 0;
Размер байтовой переменной один байт, и следовательно полный размер объекта (без заголовка) — два байта. Но размер сериализованного объекта 51 байт. Удивлены? Откуда взялись эти дополнительные байты и что они обозначают? Они добавлены сериализующим алгоритмом и необходимы для воссоздания объекта. В следующем абзаце будет подробно описан этот алгоритм.
Алгоритм сериализации Java
К этому моменту у вас уже должно быть достаточно знаний, чтобы сериализовать объект. Но как работает этот механизм? Алгоритм сериализации делает следующие вещи:
В листинге 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();
>
>
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.
Заключение
В этой статье вы увидели как сериализовать объект, и узнали как работает алгоритм сериализации. Я надеюсь эта статья помогла вам лучше понять что происходит, когда вы сериализуете объект.
Об авторе
Sathiskumar Palaniappan имеет более чем 4-х летний опыт работы в IT-индестрии, и работает с Java технологиями более 3 лет. На данный момент он работает system software engineer в Java Technology Center, IBM Labs. Также имеет опыт работы в телекоммуникационной индустрии.
Сериализация объектов в Java
Сериализация — механизм представления объекта в виде последовательности байтов, включая информацию о типе объекта в целом и типе данных сохраненных внутри объекта.
Мы все знаем, что Java позволяет нам создавать повторно используемые объекты в памяти. Однако, все эти объекты существуют только в период работы виртуальной машины. Сериализация позволяет сохранить ваши объекты для повторного использования после перезапуска программы.
Java API сериализации предоставляет стандартный механизм работы с сериализацией объектов для Java разработчиков. Этот API мал и прост в использовании, предоставляемые классы и методы понятны.
Мы посмотрим три разных пути для проведения сериализации: использование стандартного протокола, модификацию стандартного протокола и создание нашего собственного протокола, и мы исследуем, что происходит при кэшировании объектов, контроли версий, и влияние этого на производительность.
Стандартный механизм сериализации — интерфейс Serializable
Для сохранения объекта в Java, мы должны иметь сохраняемый объект. Объект делается сериализуемым путем реализации интерфейса java.io.Serializable, что значит для нижележащего API, что объект может быть сохранен в байтовом представлении и восстановлен в будущем
Интерфейса Serializable — пустой интерфейс пометка, указывающий что объект серийный. Ниже приведен максимально простой пример его использования:
Как вы можете видеть, единственным отличием от создания обычного класса является реализация интерфейса java.io.Serializable в строке 40. Полностью пустой интерфейс Serializable является маркером — это простое разрешение механизму сериализации проверять класс на возможность его сохранения. Таким образом, обратим внимание на первое правило сериализации:
Следующий шаг — это, собственно, сохранение объекта. Это выполняется классом java.io.ObjectOutputStream. Этот класс является фильтрующим потоком — это надстройка над низкоуровневым потоком байтов (называемым узловым потоком) для работы с протоколом сериализации для нас. Узловые потоки могут быть использованы для записи в файловую систему или даже в сокет. Это означает, что мы можем легко передавать сохраняемый объект по сетевому кабелю и заново создавать его на другой машине!
Посмотрите на код, используемый для сохранения объекта PersistentTime:
Классы ObjectInputStream и ObjectOutputStream — обертки для потоков ввода/вывода с поддержкой записи и загрузки объектов. Интерфейс не зависит от платформы джава машины. Т.е. объект может быть создан на jvm одной платформы и без проблем загружен на другой.
Данных механизм может использоваться для сохранения состояния приложения или его части. А также для передачи объектов по сети.
Реальная работа происходит на строке 20 когда мы вызываем метод
, который запускает механизм сериализации и объект сохраняется (в данном случае в файл).
Для восстановления из файла мы можем использовать следующий код :
В коде выше, восстановление объекта происходит в строке 21 путем вызова метода
. Вызов метода считывает байты, которые мы предварительно сохранили, и создает объект, являющийся точной копией оригинала. Так как
Позже, мы просто вызываем метод
для получения времени, которое сохранил оригинальный объект. Зафиксированное время сравнивается с текущим для демонстрации того, что механизм сериализации работает так как ожидалось.
Еще один пример сериализации объектов приведен ниже:
Базовый механизм сериализации в Java прост в использовании, но здесь есть еще несколько вещей, которые нужно знать. Как упоминалось ранее, только объекты, реализующие Serializable могут быть сохранены. Класс
не реализовывает этот интерфейс. Поэтому, не все объекты в Java могут быть сохранены автоматически. Хорошей новостью является то, что большинство из них — такие как AWT и Swing GUI компоненты, строки и массивы — сериализуемы.
Если среди членов класса есть ссылки на не серийные объекты, то при попытке сериализовать такой класс возникнет исключение NotSerializableException.
С другой стороны, некоторые системные классы, такие как Thread, OutputStream и их подклассы, а также Socket несериализуемы. В действительности это не имеет никакого смысла. Например, поток, работающий в моей JVM может использовать память моей системы. Сохранение потока и попытка его запустить в вашей JVM не имеет смысла. Другим важным моментом в том, почему
не реализует интерфейс
, является то, что любой создаваемый вами класс наследует только код Object (а не других сериализуемых классов), несериализуем до тех пор, пока вы не реализуете интерфейс самостоятельно (как сделано в предыдущем примере).
В этой ситуации присутствует проблема: что если мы имеем класс, который содержит экземпляр класса Thread? В этом случае, можем ли мы когда-либо сохранить объект этого типа? Ответ утвердителен, пока вы сообщаете механизму сериализации наши намерения маркируя наш объект класса Thread как
ключевое слово transient
Атрибут transient указывает, что указанный член класса должен игнорироваться механизмом сериализации. Полезно для секретных (пароль) или не особо важных данных.
Пусть мы хотим создать класс, производящий анимацию. Я на самом деле не приведу здесь код анимации. Вот используемый класс:
Когда мы создаем экземпляр класса
, поток animator будет создан и запущен, как мы и ожидали. Мы пометили поток модификатором
, чтобы сообщить механизму сериализации, что поле не будет сохраняться вместе с остальным состоянием объекта (в этом случае, это переменная speed). Вывод: вы должны отметить модификатором
любое поле, которое не может быть сериализованым или любое поле, которое вы не хотите сериализовывать. Сериализация не обращет внимания на модификаторы доступа, такие как
— все долгоживущие поля рассматриваются как часть сохраняемого состояния объекта и допускаются к сохранению.
Поэтому можно сформулировать следующие правила сериализации:
Другой пример использования transient: если в предыдущем примере о сериализации информации о компаниях изменить строку следующим образом:
то в результате работы программы во второй строке вместо адреса будет
Модификация стандартного протокола: переопределение сериализации
Хотя код анимации выше демонстрирует, как нить может быть включена в объект, и при этом объект остается сериализуемым, существует серьезная проблема, если мы рассмотрим, как Java создает объекты. К сведению, когда мы создаем объект с помощью ключевого слова new, конструктор объекта вызывается только тогда, когда создается новый экземпляр класса. Сохраняя этот базовый факт в памяти, вернемся к нашему коду анимации. Сначала, мы создаем объект типа
, который запускает нить анимации. Далее, мы сериализуем объект с помощью кода:
Все кажется правильным, пока мы считываем объект в вызове метода
. Запомните, конструктор вызывается только при создании нового экземпляра. Мы не создаем здесь новый экземпляр, мы восстанавливаем сохраненный объект. В итоге объект анимации будет работать только один раз, при первом создании. Метод создания не сохраняется, не так ли?
Но есть и хорошие новости. Мы можем заставить наш объект работать так, как мы хотим; мы можем сделать рестарт анимации при восстановлении объекта. Чтобы сделать это, мы можем, например, создать вспомогательный метод
, который делает то, что должен делать конструктор. Мы можем затем вызвать этот метод из конструктора, после чего мы считываем объект. Неплохо, но это приводит к увеличению сложности. Теперь, кто-то, кто хочет использовать этот объект анимации, будет знать какой метод нужно вызвать после обычного процесса десериализации. Это не делает механизм цельным, хотя Java API сериализации обещает это разработчикам.
Однако здесь имеется странное хитрое решение. Используя встроенные возможности механизма сериализации, разработчики могут усовершенствовать обычный процесс, реализуя в своих классах следующие методы (механизм сериализации будет использовать его, а не сериализовывать по умолчанию):
Эти методы объявлены (и должны быть объявлены) private, проверьте, что эти методы не уналедованы и не переопределены или не перегружены. Фокус в том, что виртуальная машина автоматически проверит, объявлены ли эти методы в течении вызова соответствующего метода. Виртуальная машина может вызывать private методы вашего класса когда она хочет, но другие объекты нет. Таким образом, целостность класса сохраняется и протокол сериализации может продолжить работу, как обычно. Протокол сериализации всегда используется тем же способом, путем вызова любого метода:
. Итак, хотя эти специализированные private методы предоставлены, сериализация объекта работает также по отношению к любому вызываемому объекту.
Принимая все это во внимание, посмотрим на исправленную версию PersistentAnimation который содержит эти private методы для предоставления нам контроля над процессом десериализаци, давая нам псевдоконструктор:
Обратите внимание на первую строку каждого из новых
методов. Эти вызовы выполняют функцию, созвучную их названию — они выполняют стандартные запись и чтение сохраняемого объекта, что важно, так как мы не заменяли обычный процесс, мы только расширяли его. Эти методы работают, так как вызов
заставляет заработать протокол сериализации. Сначала объект проверяется на реализацию
и затем проверяется, педоставлены ли эти
методы. Если они предоставлены, потоковый класс передается как параметр, давая контроль над его использованием.
методы могут быть использованы для любых изменений, которые вам нужны для выполнения сериализации. Для вывода может быть добавлено шифрование и дешифрование для ввода (заметьте, что эти байты записаны и прочитаны в виде чистого текста без каких-либо изменений). Они могут быть использованы для добавления дополнительных данных в поток, возможно кода версии фирмы. Возможности действительно не ограниченные.
Запрещаем сериализацию
Мы увидели простоту процесса сериализации, сейчас увидим несколько больше. Что если вы создаете класс, чей класс-предок сериализуем, но вы не хотите чтобы новый класс был сериализуемым? Вы не можете убрать реализацию интерфейса, следовательно если ваш класс-предок реализовал
, ваш новый класс реализовывает его тоже (объединение двух правил, приведенных выше). Для прекращения автоматической сериализации, вы снова однажды можете использовать
методы для генерации исключения
. Здесь показано, как это можно сделать:
Любая попытка записи или чтения этого объекта всегда будет генерировать исключение. Запомните, так как эти методы объявлены как private, никто не может изменить ваш код без наличия доступных исходных текстов — Java не допускает перекрытие этих методов.
Создание вашего собственного механизма сериализации: интерфейс Externalizable
Также есть возможность определить полностью свой механизм сериализации. Для нужно реализовать интерфейс Externalizable вместо
. Этот интерфейс содержит два метода:
Переопределите эти методы для предоставления вашего протокола. В отличие от предыдущих двух вариантов сериализации, здесь ничего не предоставляется. Следовательно, протокол полностью в ваших руках. Несмотря на то, что это более сложный сценарий, он также наиболее контролируемый. Пример ситуации для альтернативного типа сериализации: чтение и запись PDF файлов в Java приложении. Если вы знаете как писать и читать PDF файлы (необходимую последовательность байтов), вы можете предоставить PDF-специфичный протокол в методах
Однако, как и ранее, здесь нет различия в том, какая реализация
используется классом. Только вызовите
и, вуаля, эти методы интерфейса
будут вызваны автоматически.
Подводные камни сериализации
Имеются некоторые подводные камни механизма сериализации, которые могут выглядеть очень странно для неподготовленных разработчиков. Цель этой статьи — подготовить вас! — поговорим о некоторых затруднениях и посмотрим, почему они существуют и как их обрабатывать.
Кэширование объектов в потоке
Сначала рассмотрим ситуацию в которой объект записан в поток и затем записывается снова. По умолчанию,
будет сохранять ссылку на объект, записанный в него. Это значит, что если состояние записанного объекта записано и затем записано снова, тогда новое состояние объекта не будет сохранено! Здесь кусок кода, показывающий эту проблему в действии:
Имеется два пути для контроля этой ситуации. Первый, вы можете всегда закрывать поток после записи, гарантируя, что каждый раз записывается новый объект. Второй, вы можете вызвать метод
, который скажет потоку освободить кэш ссылок, которые он содержит, и по новому запросу на запись она будет действительно производиться. Будте внимательны, метод reset сбрасывает весь кэш объектов, значит все записанные объекты могут быть перезаписанны.
Контроль версий
Представьте, что вы создали класс, создали его экземпляр, и записали его в поток объектов. Этот сохраненный объект некоторое время находится в файловой системе. Тем временем, вы обновили файл класса, возможно добавили новое поле. Что произойдет, когда вы попытаетесь прочитать сохраненный объект?
Плохой новостью является то, что будет выработано исключение —
— так как все классы с возможностью сохраненияe автоматически получают уникальный идентификатор. Если идентификатор класса не совпадает с идентификатором сохраненного объекта, генерируется исключительная ситуация. Однако, если вы задумаетесь, почему будет вызвано исключение только из-за того, что я добавил поле? Почему полю не может быть присвоено значение по умолчанию и потом записано в следующий раз?
Да, это возможно, но потребуются небольшие манипуляции с кодом. Идентификатор, являющийся частью всех классов содержится в поле
. Если вы хотите контролировать версионность, вы просто подставляете поле
вручную и обеспечиваете ее постоянство при изменениях, вносимых в класс. Вы можете использовать утилиту serialver, поставляемую с дистрибутивом JDK, для просмотра кода по-умолчанию (по-умолчанию это хэш-код объекта).
Пример использования serialver с классом с именем
Просто скопируйте возвращенную строку с идентификатором версии и вставьте ее в ваш код. (В Windows, вы можете запустить эту утилиту с параметром
для упрощения процедуры копирования и вставки.) Теперь, если вы вносите любые изменения в класс Baz, удостоверьтесь что указан тот же идентификатор версии и все будет в порядке.
Контроль версий работает отлично пока изменения совместимы. Совместимым изменением является добавление или удаление метода или поля. Несовместимыим изменениями являются изменение иерархии объектов или удаление реализации интерфейса Serializable. Полный список совместимых и несовместимых изменений приведен в спцификации Java сериализации (смотри Ресурсы).
Рассмотрение производительности
Наше третье затруднение: стандартный механизм, несмотря на простоту использования, не лучший исполнитель. Я записывал объект
в файл 1000 раз, повторив эту процедуру 100 раз. Среднее время записи объекта
было 115 милисекунд. Затем я записал вручную объект
, используя стандартные способы ввода/вывода и то же количество итераций; среднее время было 52 милисекунды. Почти половина времени! Здесь часто возникает противоречие между удобством и производительностью, и сериализация не доказывает обратного. Если в первую очередь принимать во внимание скорость для вашего приложения, вы можете подумать о создании своего протокола.
Другим обращающим на себя внимание является вышеупомянутый факт что ссылки на объект кэшируются в потоке вывода. Соответственно этому, система может не собирать мусор из объектов, записанных в потокa, если поток не закрыт. Лучшим ходом, как и всегда с вводом-выводом, является скорейшее закрытие потоков, следующее за операциями записи.
Возможно вам будет интересно:



