Где должны быть файлы классов при десериализации объектов java
Почему существует сериализация
Мы знаем, что когда виртуальная машина перестает работать, объекты в памяти исчезают, другая ситуация заключается в том, что объекты JAVA должны передаваться в сети, такие как параметры и возвращаемые значения в процессе RMI. В обоих случаях объект должен быть преобразован в поток байтов, который используется для сохранения на диске или может быть передан по сети.
Поскольку RMI является основой технологии JAVA EE, все распределенные приложения требуют кроссплатформенности и кросс-сети. Поэтому сериализация является основой JAVA EE, Обычно рекомендуется сериализовать каждый класс JavaBean, созданный программой.
Исключение данных из сериализации
По умолчанию сериализуются все переменные объекта. Однако, возможно, мы хотим, чтобы некоторые поля были исключены из сериализации. Для этого они должны быть объявлены с модификатором transient . Например, исключим из сериализации объекта Person переменные height и married:
Как работает сериализация и десериализация в Java
Источник: Medium Я перешел на Java в январе этого года после стажировки. До этого я в основном писал на PHP и немного на JavaScript. Ранее мне не приходилось сталкиваться с сериализацией, хотя на самом деле сериализация существует и в PHP. Правда, в Java она используется намного чаще. Сегодня я познакомлю вас, как работает сериализация и десериализация в Java и расскажу о нескольких способах их применения.
Пример тестирования сериализации и десериализации
Для тестирования сериализации и десериализации объекта Person будем использовать юнит-тест JUnit, в котором создадим 2 объекта, запишем объекты в файл, после чего восстановим их. Более подробно об использовании JUnit сказано на странице Тестирование программы.
В примере ничего нового или удивительного не представлено – это основы сериализации, которые желательно знать, особенно при разработке WEB-приложений.
Как сериализовать
Если вы хотите, чтобы каждый объект поддерживал механизм сериализации, вы должны сделать его класс сериализуемым, тогда класс должен реализовать один из следующих двух интерфейсов:
Вот несколько принципов, давайте посмотрим:
1. Serializable - помеченный интерфейс, в нем не определены методы или поля, которые используются только для обозначения семантики сериализуемого.
2. Статические переменные и методы-члены не сериализуемы.
3. Для того чтобы класс был сериализуемым, все ссылочные объекты в классе также должны быть сериализуемыми. В противном случае вся операция сериализации завершится неудачно, и будет выдано исключение NotSerializableException, если мы не пометим несериализуемую ссылку как переходную.
4. Переменные, объявленные как переходные, не сохраняются инструментом сериализации, а статические переменные не сохраняются.
Сначала давайте рассмотрим сериализацию объекта и его сохранение в файле:
1. После сериализации объекта записывается двоичный файл. Нормально открывать все искаженные символы. Однако мы по-прежнему видим, что созданный нами объект сохраняется в файле через искаженные символы.
2. Объект Person реализует интерфейс Serializable.Этот интерфейс не имеет методов, которые должны быть реализованы, только интерфейс пометки, указывающий, что объекты этого класса могут быть сериализованы.
3. В этой программе мы вызываем метод writeObject () объекта ObjectOutputStream для вывода сериализуемого объекта. Этот объект также предоставляет методы для вывода основных типов.
writeFloat(float val)
Во-вторых, давайте посмотрим на процесс десериализации объектов из файла:
2. При вызове метода readObject () происходит принудительное действие. Поэтому при десериализации должен быть предоставлен файл класса класса, к которому принадлежит объект Java.
3. Если вы используете механизм сериализации для записи нескольких объектов в файл, вам необходимо прочитать их в том порядке, в котором они фактически записаны во время десериализации.
Что такое паттерн проектирования Builder?
Паттерн Builder решает проблему с большим количеством необязательных параметров и непоследовательных состояний, предоставляя способ пошагового создания объекта. Для этого используется метод, который фактически возвращает окончательный объект.
Зачем нам нужен паттерн проектирования Builder?
Наличие слишком многих аргументов для передачи из клиентской программы в класс Factory может приводить к возникновению ошибок, поскольку в большинстве случаев тип аргументов здесь один и тот же, а со стороны клиента трудно поддерживать порядок аргументов.
Некоторые параметры могут быть необязательными, но в паттерне Factory мы вынуждены отправлять все параметры, а необязательные параметры необходимо отправлять как файлы NULL.
Если объект “тяжелый” и со сложной разработкой, то все эти трудности станут частью классов Factory, что часто приводит к путанице.
Java I / O понимание (шесть) сериализация и десериализация объектов
Зачем нужен 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.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.
Использование сериализации
Технология RMI (Java Remote Method Invocation), построенная на сериализации, позволяет java-приложению, запущенному на одной виртуальной машине, вызвать методы объекта, работающего на другой виртуальной машине JVM (Java Virtual Machine).
Исходный код рассмотренного примера Serialization в виде проекта Eclipse можно скачать здесь (15.0 Kб).
Сериализация представляет процесс записи состояния объекта в поток, соответственно процесс извлечения или восстановления состояния объекта из потока называется десериализацией . Сериализация очень удобна, когда идет работа со сложными объектами.
Подписывание сериализованных данных
Чтобы убедиться, что данные не были изменены в файле или при пересылке по сети их можно «подписать». Несмотря на то, что управление подписями реализовать можно и с помощью методов writeObject и readObject, для этого есть более подходящий способ.
Если требуется зашифровать и подписать объект, то проще всего поместить его в оберточный класс javax.crypto.SealedObject и/или java.security.SignedObject. Данные классы являются сериализуемыми, поэтому при оборачивании объекта в SealedObject создается подобие "подарочной упаковки" вокруг исходного объекта. Для шифрования необходимо создать симметричный ключ, управление которым должно осуществляться отдельно. Аналогично, для проверки данных можно использовать класс SignedObject, для работы с которым также нужен симметричный ключ, управляемый отдельно. Эти два объекта позволяют упаковывать и подписывать сериализованные данные, не отвлекаясь на детали проверки и шифрования цифровых подписей.
Листинг теста подписи объекта
Десериализация. Класс ObjectInputStream
Класс ObjectInputStream отвечает за обратный процесс - чтение ранее сериализованных данных из потока. В конструкторе он принимает ссылку на поток ввода:
Функционал ObjectInputStream сосредоточен в методах, предназначенных для чтения различных типов данных. Рассмотрим основные методы этого класса:
void close() : закрывает поток
int skipBytes(int len) : пропускает при чтении несколько байт, количество которых равно len
int available() : возвращает количество байт, доступных для чтения
int read() : считывает из потока один байт и возвращает его целочисленное представление
boolean readBoolean() : считывает из потока одно значение boolean
byte readByte() : считывает из потока один байт
char readChar() : считывает из потока один символ char
double readDouble() : считывает значение типа double
float readFloat() : считывает из потока значение типа float
int readInt() : считывает целочисленное значение int
long readLong() : считывает значение типа long
short readShort() : считывает значение типа short
String readUTF() : считывает строку в кодировке UTF-8
Object readObject() : считывает из потока объект
Например, извлечем выше сохраненный объект Person из файла:
Теперь совместим сохранение и восстановление из файла на примере списка объектов:
Проблема сериализации
1. Статические переменные не будут сериализованы.
2. При сериализации подклассов:
Если родительский класс не реализует интерфейс Serializable и не предоставляет конструктор по умолчанию, сериализация подкласса пойдет не так;
Если родительский класс не реализует интерфейс Serializable и предоставляет конструктор по умолчанию, дочерний класс может быть сериализован, а переменные-члены родительского класса не будут сериализованы.
Если родительский класс реализует интерфейс Serializable, родительский и дочерний классы могут быть сериализованы.
Сериализация
Целью сериализации объекта является сохранение объекта на диске или передача объекта непосредственно по сети. Механизм сериализации объектов позволяет преобразовывать объекты Java в памяти в независимый от платформы двоичный поток, тем самым позволяя этому двоичному потоку постоянно храниться на диске и передавать по сети на другой сетевой узел, другие программы Как только этот двоичный поток получен, можно сказать, что двоичный поток восстанавливается в исходный объект JAVA.
Пользовательская сериализация
1. Как упоминалось ранее, вы можете использовать ключевое слово transient для изменения переменных экземпляра, которые будут полностью изолированы от механизма сериализации. Все еще используйте ту же процедуру, что и раньше, но измените переменную адреса с помощью transient:
2. В двоичном файле слово «Китай» не видно, а значение адреса после десериализации равно нулю.
3. Это показывает, что переменные, модифицированные с помощью tranisent, потеряют значение переменной экземпляра после сериализации и десериализации.
Ввиду вышеупомянутой ситуации JAVA предоставляет настраиваемый механизм сериализации. Таким образом, программа может контролировать, как сериализовать каждую переменную экземпляра самостоятельно, или даже не сериализовать переменные экземпляра.
Классы, которые требуют специальной обработки во время сериализации и десериализации, должны предоставлять следующие методы, которые используются для реализации пользовательской сериализации.
Эти два метода не принадлежат ни одному классу и интерфейсу, поскольку эти два метода предоставляются в сериализуемом классе, они будут автоматически вызываться в механизме сериализации.
Метод writeObject используется для записи состояния экземпляра определенного класса, чтобы соответствующий метод readObject мог его восстановить. Переписав этот метод, программисты могут получить контроль над сериализацией и могут самостоятельно решать, какие переменные экземпляра необходимо сериализовать и как сериализовать. Этот метод вызывает out.defaultWriteObject для сохранения переменной экземпляра объекта JAVA, чтобы можно было выполнить задачу сериализации состояния объекта Java.
1. Разница между этим местом и предыдущим состоит в том, что он предоставляет метод writeObject и метод readObject в классе Person и обеспечивает конкретную реализацию.
2. В процессе вызова метода writeObject в ObjectOutputStream должен быть вызван метод writeObject класса Person, поскольку журнал строки 20 в коде выводится на консоль.
3. Преимущество пользовательской реализации состоит в том, что программисты могут быть более изощренными или могут настраивать сериализацию, которую они хотят достичь, например инвертировать значение переменной адреса в примере. Используя эту функцию, мы можем выполнить специальную обработку конфиденциальной информации в процессе сериализации.
4. Здесь, поскольку мы предоставляем эти два метода в классе для сериализации, они вызываются. Если нет, я думаю, что два метода, предоставляемые ObjectOutputStream / ObjectInputStream, будут вызываться по умолчанию.
Интерфейс Serializable
Сразу надо сказать, что сериализовать можно только те объекты, которые реализуют интерфейс Serializable . Этот интерфейс не определяет никаких методов, просто он служит указателем системе, что объект, реализующий его, может быть сериализован.
Проверка десериализованного объекта, ObjectInputValidation и validateObject
Если есть необходимость выполнения контроля за значениями десериализованного/восстановленного объекта, то можно использовать интерфейс ObjectInputValidation с переопределением метода validateObject. В следующем листинге представлены изменения, которые следует внести в описание класса Person, чтобы контролировать возраст.
Если вызвать метод validateObject после десериализации объекта, то будет вызвано исключение InvalidObjectException при значении возраста за пределами 39. 60.
Сериализация. Класс ObjectOutputStream
Для сериализации объектов в поток используется класс ObjectOutputStream . Он записывает данные в поток.
Для создания объекта ObjectOutputStream в конструктор передается поток, в который производится запись:
Для записи данных ObjectOutputStream использует ряд методов, среди которых можно выделить следующие:
void close() : закрывает поток
void flush() : очищает буфер и сбрасывает его содержимое в выходной поток
void write(byte[] buf) : записывает в поток массив байтов
void write(int val) : записывает в поток один младший байт из val
void writeBoolean(boolean val) : записывает в поток значение boolean
void writeByte(int val) : записывает в поток один младший байт из val
void writeChar(int val) : записывает в поток значение типа char, представленное целочисленным значением
void writeDouble(double val) : записывает в поток значение типа double
void writeFloat(float val) : записывает в поток значение типа float
void writeInt(int val) : записывает целочисленное значение int
void writeLong(long val) : записывает значение типа long
void writeShort(int val) : записывает значение типа short
void writeUTF(String str) : записывает в поток строку в кодировке UTF-8
void writeObject(Object obj) : записывает в поток отдельный объект
Эти методы охватывают весь спектр данных, которые можно сериализовать.
Например, сохраним в файл один объект класса Person:
Сериализация не безопасна
Двоичный формат сериализации полностью документирован и обратим. Для того, чтобы определить параметры объекта и его значения, достаточно просто вывести содержимое сериализованного потока в консоль. Это имеет некоторые связанные с безопасностью неприятные последствия. Например, при выполнении удаленного вызова метода с помощью RMI все закрытые поля пересылаемых по сети объектов выглядят в потоке сокета почти как обычный текст, что, конечно же, нарушает даже самые простые правила безопасности.
К счастью разработчиков Java Serialization API позволяет "вклиниться" в процесс сериализации, и изменить (или запутать) поля данных как перед сериализацией, так и после десериализации. Это можно сделать, определив методы writeObject, readObject объекта Serializable.
Изменение сериализованных данных, writeObject, readObject
Чтобы изменить процесс сериализации в классе Person реализуем метод writeObject. Для модифицирования процесса десериализации определим в том же классе метод readObject. При реализации этих методов необходимо корректно восстановить данные.
В данных методах значение возраста age просто умножается на 4 при записи объекта, а при восстановлении - делится на четыре. Алгоритм, конечно же можно усложнить; в примере он представлен только для демонстрации возможностей сериализации.
Класс сериализации Person
В следующем листинге показан класс Person, реализующий интерфейс Serializable.
Далее класс Person будет использоваться в примерах, чтобы показать дополнительные возможности, связанные с сериализацией Java-объектов.
Модификатор поля transient
Использование при описании поля класса модификатора transient позволяет исключить указанное поле из сериализации. Это бывает полезно для секретных (пароль) или не особо важных данных. Если, например, при описании объекта Person включить следующее поле address
то в результате сериализации и десериализации адрес объекта принимает значение по умолчанию или будет null.
Модификатор transient действует только на стандартный механизм сериализации Serializable. При использовании Externalizable никто не мешает сериализовать это поле, равно как и использовать его для определения других полей.
Модификатор поля static
При стандартной сериализации поля, имеющие модификатор static, не сериализуются. Соответственно, после десериализации это поле значения не меняет. При использовании реализации Externalizable сериализовать и десериализовать статическое поле можно, но не рекомендуется этого делать, т.к. это может сопровождаться трудноуловимыми ошибками.
Модификатор поля final
Поля с модификатором final сериализуются как и обычные. За одним исключением – их невозможно десериализовать при использовании Externalizable, поскольку final-поля должны быть инициализированы в конструкторе, а после этого в readExternal изменить значение этого поля будет невозможно. Соответственно, если необходимо сериализовать объект с final-полем неоходимо использовать только стандартную сериализацию.
Прокси-класс для сериализации
Иногда класс может включать элемент, который позволяет получить значения отдельных полей класса по определенному алгоритму. В этих случаях необязательно сериализовывать весь объект. Можно было бы пометить восстанавливаемые поля как «транзитные». Однако в классе всё равно требуется явно указывать код (определять метод), который при обращении к полю каждый раз проверял бы его инициализацию. Для этих целей лучше использовать специальный прокси-класс, из которого можно восстановить объект.
Листинг прокси-класса
В прокси-классе определим метод readResolve, которой будет вызываться во время десериализации объекта, чтобы вернуть объект-замену. Конструктор прокси-класса будет упаковывать объект PersonY во внутреннее поле data.
Класс PersonY создадим на основе базового класса Person с добавлением метода writeReplace следующего вида :
Вместе методы writeReplace и readResolve позволяют классу PersonY упаковывать все данные (или их наиболее важную часть) в объект класса PersonProxy, помещать его в поток и затем распаковать его при десериализации.
Пример тестирования прокси-класса JUnitPersonProxy
Как реализовать паттерн проектирования Builder в Java?
Создайте статический вложенный класс ( static nested class ) как класс Builder , а затем скопируйте все поля из внешнего класса в класс Builder . Мы должны следовать соглашению об именах, поэтому если имя класса Person , то класс Builder должен называться как PersonBuilder .
Класс Builder должен иметь общедоступный конструктор со всеми необходимыми полями в качестве параметров.
Класс Builder должен иметь методы для установки необязательных параметров, и он должен возвращать тот же объект Builder после установки необязательного поля.
Последним шагом является предоставление метода build() в классе Builder , который будет возвращать объект, необходимый клиентской программе. Для этого нам нужно иметь частный конструктор в основном классе с классом Builder в качестве аргумента.
Пример:
Давайте рассмотрим пример, чтобы получить четкое представление о паттерне проектирования Builder . Пример шаблона Builder : в java.lang.StringBuilder и java.lang.StringBuffer использовали шаблон Builder для построения объектов.
Переопределение сериализации в интерфейсе Serializable
Если у сериализуемого объекта реализован один из следующих методов, то механизм сериализации будет использовать его, а не метод по умолчанию :
- writeObject - запись объекта в поток;
- readObject - чтение объекта из потока;
- writeReplace - позволяет заменить себя экземпляром другого класса перед записью;
- readResolve - позволяет заменить на себя другой объект после чтения.
Ниже приводятся примеры использования данных методов.
Что такое сериализация и десериализация
Сериализация — это преобразование объекта из класса в последовательность байтов на виртуальной машине Java (JVM) для передачи на другую виртуальную машину Java. Если виртуальная машина Java воссоздает объект из байтов, то этот процесс называется десериализацией.
Сериализация ссылок на объекты
1. Переменные-члены объектов, представленные выше, являются основными типами данных. Если переменные-члены объекта являются ссылочными типами, будут ли они другими?
Переменные-члены этого ссылочного типа также должны быть сериализуемыми, в противном случае объекты класса с переменными-членами этого типа не могут быть сериализованы.
2. В эталонном объекте будет особая ситуация. Например, есть два объекта Teacher, и их переменные экземпляра Student оба ссылаются на один и тот же объект Person, и объект Person также имеет ссылочную переменную для ссылки на него. Как показано ниже:
Существует три объекта per, t1 и t2. Если все они сериализованы, возникнет такая проблема. При сериализации t1 объект person будет неявно сериализован. При сериализации t2 объект person также неявно сериализуется. При сериализации per объект person будет явно сериализован. Следовательно, при десериализации вы получите три объекта-человека, что приведет к тому, что объекты-люди, на которые ссылаются t1 и t2, будут разными. , Очевидно, что это не соответствует отношениям, показанным на рисунке, а также нарушает первоначальное намерение сериализации Java.
Чтобы избежать этого, механизм сериализации JAVA использует специальный алгоритм:
1. Все объекты, сохраненные на диске, имеют серийный номер.
2、 Когда программа пытается сериализовать объект, она сначала проверит, сериализован ли объект. Только если объект никогда не сериализовался (в этой виртуальной машине), система преобразует объект в последовательность байтов и Вывод.
3. Если объект был сериализован, программа напрямую выведет серийный номер вместо повторной сериализации.
Сериализация и рефакторинг кода
Сериализация позволяет вносить небольшие изменения в структуру класса, так что даже после рефакторинга класс ObjectInputStream по-прежнему будет с ним прекрасно работать. К наиболее важным изменениям, с которыми спецификация Java Object Serialization может справляться автоматически:
- добавление новых полей в класс;
- изменение полей из статических в нестатические;
- изменение полей из транзитных в нетранзитные.
Обратные изменения (нестатических и нетранзитных полей в статические и транзитные) или удаление полей требуют определенной дополнительной обработки в зависимости от того, какая степень обратной совместимости требуется.
Рефакторинг сериализованного класса
Для тестирования сериализации с измененной структурой класса Person на основе предыдущего примера необходимо проделать следующие предварительные шаги :
- Создать файл данных с использованием метода setUpBeforeClass класса JUnitPerson и блокировкой удаления файла в методе tearDownAfterClass; можно закоментировать строку удаления файла.
- Блокировать создание файла данных в методе setUpBeforeClass класса JUnitPerson при следующем выполнении; также можно закоментировать строки создание файла.
- Изменить код класса Person, как это представлено в следующем листинге.
Листинг изменений класса Person
Теперь можно выполнить тест testSerialization класса JUnitPerson и увидеть, что тест прошел успешно, т.е. класс ObjectInputStream прочитал данные и объект был восстановлен корректно. При желании можно в конце кода testSerialization вставить строку
чтобы убедиться, что значения объекта восстановлены правильно, и что параметр gender равен null.
Пример сериализации и десериализации
Сериализация
Десериализация
Десериализация является обратным действием по отношению к сериализации. Для восстановления объекта из последовательности байтов используется ObjectInputStream и метод readObject . Заметьте, что для обеспечения доступа к полям в классе Person объект приведен к типу данных Person . Объект класса, который не реализует интерфейс сериализации, не может быть сериализован. Поэтому любой класс, который ссылается на класс, реализующий интерфейс сериализации, должен сам реализовывать интерфейс сериализации, иначе будет выдано исключение. Сериализация не зависит от платформы, то есть сериализуемый поток байтов может десериализоваться другой виртуальной машиной Java. Статические и переходные поля не сериализуются, поэтому если у вас есть поле, которое вы не хотите сериализовать, сделайте его временным или статическим. В случае статического поля оно не сериализуется, потому что статическое поле принадлежит классу, а не объекту. Из-за этого переходное состояние предотвращает сериализацию поля. Сериализация применяется в Hibernate, JPA и RMI. Сериализацию также можно настроить. Это называется пользовательской сериализацией.
Прежде всего, вопрос на засыпку. А сколько существует способов сделать объект сериализуемым? Практика показывает, что более 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. Иначе поля дочернего класса сериализованы не будут. В этом случае надо бы не забыть вызвать родительские методы, иначе не сериализованы будут уже родительские поля. * * * С деталями, пожалуй, закончили. Однако есть один вопрос, который мы не затронули, глобального характера. А именно –
Особенности сериализации Date
Если объект сериализации включает поля типа Date, то здесь следует, при необходимости, учитывать временной сдвиг между различными регионами. В противном случае может возникнуть ситуация, при которой на сервере будет создан объект Date с одним временем в определенной TimeZone, а на клиенте он будет десериализован с другой TimeZone. В зависимости от времени создания игнорирование TimeZone может привести к изменению даты. Решение данной проблемы представлено на странице описания TimeZone.
Читайте также: