Serializable java что это
32. Java — Сериализация
Java предоставляет механизм, называемый сериализацией объектов, в котором объект может быть представлен в виде последовательности байтов, которая включает в себя данные об объекте, а также информацию о типе объекта и типах данных, хранящихся в объекте.
Содержание
После того, как сериализованный объект был записан в файл, он может быть прочтен из файла и десериализован, то есть информацию о типе и байты, которые представляют объект и его данные, можно использовать для воссоздания объекта в памяти.
Больше всего впечатляет то, что весь процесс независим от JVM (виртуальной машины Java), то есть объект может быть сериализован на одной платформе и десериализован на совершенно другой платформе.
Класс ObjectOutputStream содержит много методов записи для осуществления записи различных типов данных, но среди них в особенности выделяется один метод:
Вышеуказанный метод осуществляет сериализацию Объекта и отправляет его в поток выходных данных. Аналогично, класс ObjectInputStream содержит следующий метод осуществления десериализации объекта:
Чтобы продемонстрировать, каким образом сериализация работает в Java, я собираюсь использовать класс Employee (сотрудник), который обсуждался в начале книги. Предположим, что мы располагаем следующим классом Employee, который внедряет сериализируемый интерфейс.
Пример
Обратите внимание, что для успешного выполнения сериализации класса должны быть выполнены два условия:
Сериализация объекта
Класс ObjectOutputStream используется для выполнения сериализации объекта. Следующая программа SerializeDemo создает экземпляр объекта Employee и сериализует его в файл.
По завершению выполнения программы создается файл с именем employee.ser. Программа не генерирует никаких выходных данных, но изучает код и пытается определить, что совершает программа.
Примечание: в Java при сериализации объекта в файл установленным архитектурным требованием Java является присвоение файлу расширения .ser.
Пример
Десериализация объекта
Следующая программа DeserializeDemo выполняет в Java десериализацию объекта Employee, созданного в программе SerializeDemo. Изучите программу и попытайтесь определить ее выводимые данные.
Пример
Это даст следующий результат:
Следующие важные моменты, которые следует отметить, представлены ниже:
Все, что вам нужно знать о Java-сериализации
Мы будем использовать ниже Employee объект класса в качестве примера для объяснения
Что такое сериализация и десериализация?
В Java мы создаем несколько объектов, которые живут и умирают соответственно, и каждый объект обязательно умрет, когда JVM умрет. Но иногда нам может потребоваться повторно использовать объект между несколькими JVM или мы можем перенести объект на другой компьютер по сети.
Что ж, сериализация позволяет нам преобразовывать состояние объекта в поток байтов, который затем можно сохранить в файл на локальном диске или отправить по сети на любой другой компьютер. А десериализация позволяет нам обратить процесс вспять, что означает повторное преобразование сериализованного байтового потока в объект снова.
Проще говоря, сериализация объекта — это процесс сохранения состояния объекта в последовательности байтов, а десериализация — процесс восстановления объекта из этих байтов. Как правило, полный процесс называется сериализацией, но я думаю, что для большей ясности лучше классифицировать их как отдельные:
Процесс сериализации не зависит от платформы, объект, сериализованный на одной платформе, может быть десериализован на другой платформе.
Только классы, которые реализуют Serializable, могут быть сериализированы
Подобно интерфейсу Cloneable для клонирования Java в сериализации, у нас есть один интерфейс маркера, Serializable, который работает как флаг для JVM. Любой класс, который реализует Serializable интерфейс напрямую или через своего родителя, может быть сериализован, а классы, которые не реализуют, Serializable не могут быть сериализованы.
Почему сериализуемый объект не реализуется объектом?
Теперь возникает вопрос: если Serialization является очень базовой функциональностью, и любой класс, который не реализует, Serializable не может быть сериализован, то почему Serializable не реализуется сам по Object себе? Таким образом, все наши объекты могут быть сериализованы по умолчанию.
Переходные и статические поля не сериализуются
Все статические поля принадлежат классу, а не объекту, и процесс сериализации сериализует объект, поэтому статические поля не могут быть сериализованы.
Что такое serialVersionUID? И почему мы должны это объявлять?
Предположим, у нас есть класс, и мы сериализовали его объект в файл на диске, и из-за некоторых новых требований мы добавили / удалили одно поле из нашего класса. Теперь, если мы попытаемся десериализовать уже сериализованный объект, мы получим InvalidClassException ; Почему?
Если мы изменим структуру нашего класса, например поля удаления / добавления, этот номер версии также изменится, и в соответствии с JVM наш класс не будет совместим с версией класса сериализованного объекта. Вот почему мы получаем исключение, но если вы действительно думаете об этом, почему оно должно быть выброшено только потому, что я добавил поле? Не может ли поле просто установить его значение по умолчанию, а затем записать в следующий раз?
Да, это можно сделать, предоставив serialVersionUID поле вручную и убедившись, что оно всегда одинаково. Настоятельно рекомендуется, чтобы каждый сериализуемый класс объявлял его serialVersionUID как сгенерированный класс зависимым от компилятора и, следовательно, может привести к непредвиденным последствиям . InvalidClassExceptions
Вы можете использовать утилиту, поставляемую с дистрибутивом JDK,
serialver для просмотра того, каким будет этот код по умолчанию (это просто хэш-код объекта по умолчанию).
Настройка сериализации и десериализации с помощью методов writeObject и readObject
JVM полностью контролирует сериализацию объекта в процессе сериализации по умолчанию, но есть много недостатков в использовании процесса сериализации по умолчанию, некоторые из которых:
Но мы можем переопределить это поведение сериализации по умолчанию внутри нашего Java-класса и предоставить некоторую дополнительную логику для улучшения нормального процесса. Это можно сделать, предоставив два метода writeObject и readObject внутри класса, который мы хотим сериализовать:
Объявление обоих методов как закрытых необходимо (общедоступные методы не будут работать), поэтому, кроме JVM, ничто другое не сможет их увидеть. Это также доказывает, что ни один метод не наследуется, не переопределяется и не перегружается. JVM автоматически проверяет эти методы и вызывает их в процессе сериализации-десериализации. JVM может вызывать эти закрытые методы, но другие объекты не могут. Таким образом, целостность класса сохраняется, и протокол сериализации может продолжать работать в обычном режиме.
Мы можем вызывать ObjectOutputStream.defaultWriteObject() и ObjectInputStream.defaultReadObject() из этих методов получить логику сериализации по умолчанию. Эти вызовы делают то, на что они похожи — они выполняют запись и чтение сериализованного объекта по умолчанию, что важно, потому что мы не заменяем обычный процесс; мы только добавляем к этому.
Эти закрытые методы могут использоваться для любой настройки, которую вы хотите выполнить в процессе сериализации, например, шифрование может быть добавлено к выводу и дешифрование ко входу (обратите внимание, что байты записываются и читаются в виде открытого текста без всякой обфускации). Они могут быть использованы для добавления дополнительных данных в поток, возможно, код версии компании, возможности действительно безграничны.
Остановка сериализации и десериализации
Заключение
Сериализация
1. Что такое сериализация и десериализация
Java Serialization API предоставляет стандартный механизм для создания сериализуемых объектов. Процесс сериализации заключается в сериализации каждого поля объекта, но только в том случае, если это поле не имеет спецификатора static или transient. Поля, помеченные ими не могут быть предметом сериализации.
Для того, что бы объект мог быть сериализован, он должен реализовать интерфейс Serializable. Интерфейс java.io.Serializable не определяет никаких методов. Его присутствие только определяет, что объекты этого класса разрешено сериализовывать. При попытке сериализовать объект, не реализующий этот интерфейс, будет брошено java.io.NotSerializableException.
После того как объект был сериализован (превращен в последовательность байт), его можно восстановить, при этом восстановление можно проводить на любой машине (вне зависимости от того, где проводилась сериализация).
При десериализации поле со спецификатором transient получает значение по умолчанию, соответствующее его типу.
Поле со спецификатором static не изменяется.
Десериализация происходит следующим образом: под объект выделяется память, после чего его поля заполняются значениями из потока. КОНСТРУКТОР объекта при этом НЕ ВЫЗЫВАЕТСЯ.
Для работы по сериализации в java.io определены интерфейсы ObjectInput, ObjectOutput и реализующие их классы ObjectInputStream и ObjectOutputStream соответственно. Для сериализации объекта нужен выходной поток OutputStream, который следует передать при конструировании ObjectOutputStream. После чего вызовом метода writeObject() сериализовать объект и записать его в выходной поток.
2. Граф исходного объекта
Сериализуемый объект может хранить ссылки на другие объекты, которые в свою очередь так же могут хранить ссылки на другие объекты. И все ссылки тоже должны быть восстановлены при десериализации. Важно, что если несколько ссылок указывают на один и тот же объект, то в восстановленных объектах эти ссылки так же указывали на один и тот же объект. Чтобы сериализованный объект не был записан дважды, механизм сериализации некоторым образом для себя помечает, что объект уже записан в граф, и когда в очередной раз попадется ссылка на него, она будет указывать на уже сериализованный объект. Такой механизм необходим, что бы иметь возможность записывать связанные объекты, которые могут иметь перекрестные ссылки. В таких случаях необходимо отслеживать, был ли объект уже сериализован, то есть нужно ли его записывать или достаточно указать ссылку на него.
3. Ключевое слово transient
Если по какой-то причине класс не может реализовать интерфейс Serializable, переменная этого класса может быть объявлена как transient.
4. Десериализация и наследование
При десериализации производного класса, наследуемого от несериализуемого класса, вызывается конструктор без параметров родительского НЕ сериализуемого класса. И если такого конструктора не будет – при десериализации возникнет ошибка java.io.InvalidClassException. Конструктор же дочернего объекта, того, который мы десериализуем, не вызывается.
В процессе десериализации, поля НЕ сериализуемых классов (родительских классов, НЕ реализующих интерфейс Serializable) инициируются вызовом конструктора без параметров. Такой конструктор должен быть доступен из сериализуемого их подкласса. Поля сериализуемого класса будут восстановлены из потока.
Serializable java что это
Java Serialization API используется множеством других Java API (например, RMI и JavaBeans) для сохранения объектов за пределами жизненного цикла виртуальной машины. Вы также можете самостоятельно использовать Java Serialization API для сохранения объектов в собственных целях. Несмотря на простоту основ сериализации Java, в использовании API существуют некоторые сложные моменты. В этой статье Тодд Гриньер (Todd Greanier) откроет вам секреты использования Java Serialization API.
Все мы знаем о том, что Java позволяет создавать в памяти объекты для многократного использования. Однако все эти объекты существуют лишь до тех пор, пока выполняется создавшая их виртуальная машина. Было бы неплохо, если бы создаваемые нами объекты могли бы существовать и вне пределов жизненного цикла виртуальной машины, не так ли? Что же, используя сериализацию объектов вы можете разложить свои объекты на байты и затем использовать их наиболее эффективным образом.
В этой статье мы увидим каким образом можно сохранять Java объекты и, начав с основ, закончим знакомством с более сложными концепциями. Мы рассмотрим три различных способа выполнения сериализации (используя протокол по умолчанию, изменяя протокол по умолчанию и создавая свой собственный протокол) и познакомимся с ситуациями, возникающими при использовании этих схем сохранения, такими как кэширование объектов, контроль версий и проблемы с быстродействием.
Прочитав статью вы получите полное представление об этом мощном, но зачастую плохо понимаемом Java API.
Начнем с начала: Механизм используемый по умолчанию
Давайте начнем с основ. Для сохранения объекта в Java мы должны иметь объект, нуждающийся в сохранении и этот объект должен быть отмечен как сериализуемый. Это осуществляется путем реализации объектом интерфейса java.io.Serializable, что является для API знаком того, что объект может быть разложен на байты, а затем вновь восстановлен.
Давайте взглянем на сохраняемый класс, используемый в нашей статье для демонстрации механизма сериализации:
Правило №1: Сохраняемый объект должен реализовать интерфейс Serializable или унаследовать эту реализацию от вышестоящего по иерархии объекта.
Давайте взглянем на код, используемый для сохранения объекта PersistentTime:
Реальная работа выполняется в 200-й строке, когда мы вызываем метод ObjectOutputStream.writeObject(), который запускает механизм сериализации и объект разлагается на байты (в данном случае в файл).
Для восстановления объекта из файла можно использовать следующий код:
В этом коде в 210-й строке происходит восстановление объекта при помощи вызова метода ObjectInputStream.readObject(). Метод считывает последовательность байтов, которую мы перед этим сохранили в файле, и создает «живой» объект, полностью повторяющий оригинал. Поскольку readObject() может считывать любой сериализуемый объект, необходимо его присвоение соответствующему типу. Таким образом, из системы, в которой происходит восстановление объекта, должен быть доступен файл класса. Другими словами, при сериализации не сохраняется ни файл класса объекта, ни его методы, сохраняется лишь состояние объекта.
Затем, в 360-й строке, мы просто вызываем метод getTime(), чтобы получить время у разложенного объекта. Время разложенного объекта сравнивается с текущим временем, дабы показать что механизм действительно работает так, как мы ожидаем.
Несериализуемые объекты
Такая ситуация вызывает проблему: что если у нас есть класс, который содержит экземпляр Thread? Можем ли мы в этом случае сохранить объект такого типа? Ответ положительный, поскольку мы имеем возможность сообщить механизму сериализации о своих намерениях, пометив объект Thread нашего класса как нерезидентный (transient).
Предположим, нам нужно создать класс, выполняющий анимацию. В своем примере я не стал приводить код анимации, ограничившись только общим описанием класса:
При создании экземпляра класса PersistentAnimation создается и запускается поток animator. В 40-й строке мы пометили этот поток как transient, дабы сообщить механизму сериализации о том, что поле не должно сохраняться вместе с остальными состояниями этого объекта (в нашем случае, полем speed). Резюме: вы должны помечать как transient все поля, которые либо не могут быть сериализованы, либо те, которые вы не хотите сериализовать. Сериализация не заботится о модификаторах доступа, таких как private. Все резидентные поля рассматриваются как части состояния сохраняемого объекта, предназначенные для сохранения.
Следовательно нам нужно добавить еще одно правило. Итак, вот оба правила относительно сохраняемых объектов:
Изменение протокола по умолчанию
Давайте перейдем ко второму способу реализации сериализации: изменение протокола по умолчанию. Хотя в анимационном коде, рассмотренном выше, был показан способ использования потока с объектом, обеспечив при этом его сериализацию, для того чтобы понять суть проблемы нужно разобраться в том, каким образом Java создает объекты. Задумайтесь, когда мы создаем объект при помощи ключевого слова new, конструктор объекта вызывается только при создании нового экземпляра объекта. Запомним этот факт и вновь взглянем на наш анимационный код. Сначала мы создаем экземпляр объекта PersistentAnimation, который запускает поток анимации. Затем мы сериализуем его при помощи кода:
Все кажется в порядке, но только до тех пор, пока мы не прочитаем объект используя вызов метода readObject(). Помните, конструктор вызывается только при создании нового экземпляра объекта. Здесь же мы не создаем нового экземпляра, мы просто восстанавливаем сохраненный объект. В результате анимационный объект отработает лишь однажды, при первом создании экземпляра этого объекта, что делает процесс его сохранения бессмысленным, не так ли?
Что же, есть и хорошая новость. Мы можем заставить наш объект работать так, как нам хочется, перезапуская анимацию при восстановлении объекта. Чтобы сделать это мы можем, например, создать вспомогательный метод startAnimation(), выполняющий те же функции, что и наш конструктор. Затем мы можем вызывать этот метод из конструктора, после каждой загрузки объекта. Неплохо, но несколько сложно. Теперь все, кто захочет использовать анимационный объект, должны знать о необходимости вызова этого метода после обычного процесса десериализации, что никак не вписывается в тот единообразный механизм, который Java Serialization API обещает разработчикам.
Однако, существует другое странное и хитрое решение. Используя встроенную возможность механизма сериализации, разработчики могут реализовать нормальный процесс поместив в свои файлы классов два метода:
Обратите внимание, что оба метода (совершенно справедливо), объявлены как private, поскольку это гарантирует что методы не будут переопределены или перезагружены. Весь фокус в том, что виртуальная машина при вызове соответствующего метода автоматически проверяет, не были ли они объявлены в классе объекта. Виртуальная машина в любое время может вызвать private методы вашего класса, но другие объекты этого сделать не смогут. Таким образом обеспечивается целостность класса и нормальная работа протокол сериализации. Протокол сериализации всегда используется одинаково, путем вызова ObjectOutputStream.writeObject() или ObjectInputStream.readObject(). Таким образом, даже если в классе присутствуют эти специализированные private методы, сериализация объектов будет работать так же, как и для любых других вызываемых объектов.
Учитывая это, давайте взглянем на исправленную версию PersistentAnimation, в которую включены эти private методы для контроля над процессом десериализации через псевдо-конструктор:
Остановите сериализацию!
О’кей, мы уже кое-что узнали о процессе сериализации, а теперь давайте двигаться дальше. Что если вы создали класс, чей суперкласс сериализуемый, но при этом вы не хотите чтобы ваш класс был сериализуемым? Вы не можете «разреализовать» интерфейс, поэтому если суперкласс реализует Serializable, то и созданный вами новый класс также будет реализовать его (в соответствии с двумя рассмотренными выше правилами). Чтобы остановить автоматическую сериализацию вы можете снова применить private методы для создания исключительной ситуации NotSerializableException. Вот как это можно сделать:
Любая попытка записать или прочитать этот объект теперь приведет к возникновению исключительной ситуации. Запомните, если методы объявлены как private, никто не сможет модифицировать ваш код не изменяя исходный код класса. Java не позволяет переопределять такие методы.
Создание своего собственного протокола: интерфейс Externalizable
Наше обсуждение было бы неполным без упоминания третьей возможности сериализации: создания собственного протокола с интерфейсом Externalizable. Вместо реализации интерфейса Serializable, вы можете реализовать интерфейс Externalizable, который содержит два метода:
Для создания собственного протокола нужно просто переопределить эти два метода. В отличие от двух рассмотренных ранее вариантов сериализации, здесь ничего не делается автоматически. Протокол полностью в ваших руках. Хотя это и наболее сложный способ, при этом он наиболее контролируемый. Возьмем, к примеру, ситуацию с альтернативным типом сериализации: запись и чтение PDF файлов Java приложением. Если вы знаете как читать и записывать PDF файлы (требуется определенная последовательность байт), вы можете создать протокол с учетом специфики PDF используя методы writeExternal и readExternal.
Так же, как и в рассмотренных случаях, нет никакой разницы в том, как используется класс, реализующий Externalizable. Вы просто вызываете методы writeObject() или readObject() и, вуаля, эти расширяемые методы будут вызываться автоматически.
Нюансы
Существует несколько моментов, имеющих отношение к протоколу сериализации, которые могут показаться очень странными для непосвященного разработчика. Разумеется, цель этой статьи посвятить вас в них! Поэтому давайте обсудим некоторые нюансы, попробуем понять почему они появились и как с ними обращаться.
Кэширование объектов в потоке
Во-первых, рассмотрим ситуацию, когда объект однажды уже записанный в поток, спустя какое-то время записывается в него снова. По умолчанию, ObjectOutputStream сохраняет ссылки на объекты, которые в него записываются. Это означает, что если состояние записываемого объекта, который уже был записан, будет записано снова, новое состояние не сохраняется! Следующий фрагмент кода демонстрирует эту проблему:
Существует два способа взять ситуацию под контроль. Во-первых, вы можете каждый раз после вызова метода записи убеждаться в том, что поток закрыт. Во-вторых, вы можете вызвать метод ObjectOutputStream.reset(), который сигнализирует потоку о том, что необходимо освободить кэш от ссылок, которые он хранит, чтобы новые вызовы методов записи действительно записывали данные. Будьте осторожны, reset очищает весь кэш объекта, поэтому все ранее записанные объекты могут быть перезаписаны заново.
Контроль версий
Для второго случая представим что мы создали класс, затем создали его экземпляр, который записали в поток объекта. Этот разложенный на байты объект какое-то время находился в файловой системе. Тем временем вы обновляете файл класса, например, добавив в него новое поле. Что произойдет если затем вы попробуете прочитать разложенный объект?
Плохая новость заключается в том, что возникнет исключительная ситуация, а именно java.io.InvalidClassException, потому что всем классам, которые могут быть сохранены, присваивается уникальный идентификатор. Если идентификатор класса не совпадает с идентификатором разложенного объекта, возникает исключительная ситуация. Однако, если задуматься над этим, зачем нужны все эти исключительные ситуации, если вы всего лишь добавили новое поле? Разве нельзя установить в поле значение по умолчанию, а после сохранено?
Да, но это потребует легких манипуляций с кодом. Идентификатор, который является частью всех классов, хранится в поле, которое называется serialVersionUID. Если вы хотите контролировать версии, вы должны вручную задать поле serialVersionUID и убедиться в том, что оно такое же, и не зависит от изменений, внесенных вами в объект. Вы можете использовать утилиту, входящую в состав JDK, которая называется serialver, чтобы посмотреть какой код будет присвоен по умолчанию (это просто hash код объекта по умолчанию).
Вот пример использования serialver с классом Baz:
Просто скопируйте возвращенную строку с идентификатором версии и поместите ее в ваш код. (В Windows вы можете запустить эту утилиту с опцией -show, чтобы упростить процедуру копирования и вставки). Теперь, если вы внесли какие-либо изменения в файл класса Baz, просто убедитесь что указан тот же идентификатор версии и все будет в порядке.
Контроль за версией прекрасно работает до тех пор, пока вносимые изменения совместимы. К совместимым изменениям относят добавление и удаление методов и полей. К несовместимым изменениям относят изменение иерархии объектов или прекращение реализации интерфейса Serializable. Полный перечень совместимых и несовместимых изменений приведен в Спецификации сериализации Java (см. Ссылки).
Обсуждение производительности
Заключение
Сериализация в Java проста для понимания и почти настолько же проста в реализации. Знание трех различных способов реализации сериализации должно помочь вам привязке API к вашей задаче. В этой статье вы познакомились с множеством механизмов сериализации и, я надеюсь, это пролило свет на эти вопросы, а не запутало вас еще больше. И, после прочтения, как это бывает в программировании, у вас появилось такое чувство, что вы давно знакомы с этим API. В статье изложены основы API Java сериализации, но перед использованием спецификации я рекомендую получше изучить ее во всех подробностях
Об авторе
Тодд Гриньер (Todd Greanier), технический директор ComTech Training приступил к обучению и разработке на языке Java сразу же, как только тот был представлен широкой общественности. Являясь экспертом по распределенным Java технологиям, он проводил обучение работе с классами для широчайшего спектра тематик, включая JDBC, RMI, CORBA, UML, Swing, сервлеты/JSP, безопасность, JavaBeans, Enterprise Java Beans и многопоточность. Он также организовывал специальные семинары для корпораций с учетом специфики их требований. Тодд живет на севере шатата Нью-Йорк вместе с женой Стэйси (Stacey) и кошкой Бин (Bean).