Главная страница » Что такое сериализация в программировании

Что такое сериализация в программировании

  • автор:

Сериализация в Java

Сериализация (Serialization) — процесс преобразования структуры данных в линейную последовательность байтов для дальнейшей передачи или сохранения. Сериализованные объекты можно затем восстановить (десериализовать).

В Java, согласно спецификации Java Object Serialization существует два стандартных способа сериализации: стандартная сериализация, через использование интерфейса java.io.Serializable и «расширенная» сериализация — java.io.Externalizable .

Сериализация позволяет в определенных пределах изменять класс. Вот наиболее важные изменения, с которыми спецификация Java Object Serialization может справляться автоматически:

  • добавление в класс новых полей;
  • изменение полей из статических в нестатические;
  • изменение полей из транзитных в нетранзитные.

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

Опишите процесс сериализации/десериализации с использованием Serializable .

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

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

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

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

Как изменить стандартное поведение сериализации/десериализации?

  • Реализовать интерфейс java.io.Externalizable , который позволяет применение пользовательской логики сериализации. Способ сериализации и десериализации описывается в методах writeExternal() и readExternal() . Во время десериализации вызывается конструктор без параметров, а потом уже на созданном объекте вызывается метод readExternal .
  • Если у сериализуемого объекта реализован один из следующих методов, то механизм сериализации будет использовать его, а не метод по умолчанию :
    • writeObject() — запись объекта в поток;
    • readObject() — чтение объекта из потока;
    • writeReplace() — позволяет заменить себя экземпляром другого класса перед записью;
    • readResolve() — позволяет заменить на себя другой объект после чтения.

    Как исключить поля из сериализации?

    Для управления сериализацией при определении полей можно использовать ключевое слово transient , таким образом исключив поля из общего процесса сериализации.

    Что обозначает ключевое слово transient ?

    Поля класса, помеченные модификатором transient , не сериализуются.

    Обычно в таких полях хранится промежуточное состояние объекта, которое, к примеру, проще вычислить. Другой пример такого поля — ссылка на экземпляр объекта, который не требует сериализации или не может быть сериализован.

    Какое влияние оказывают на сериализуемость модификаторы полей static и final

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

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

    Как не допустить сериализацию?

    Чтобы не допустить автоматическую сериализацию можно переопределить private методы для создания исключительной ситуации NotSerializableException .

    Любая попытка записать или прочитать этот объект теперь приведет к возникновению исключительной ситуации.

    Как создать собственный протокол сериализации?

    Для создания собственного протокола сериализации достаточно реализовать интерфейс Externalizable , который содержит два метода:

    Какая роль поля serialVersionUID в сериализации?

    serialVersionUID используется для указания версии сериализованных данных.

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

    Рекомендуется явно объявлять serialVersionUID т.к. при добавлении, удалении атрибутов класса динамически сгенерированное значение может измениться и в момент выполнения будет выброшено исключение InvalidClassException .

    Когда стоит изменять значение поля serialVersionUID ?

    serialVersionUID нужно изменять при внесении в класс несовместимых изменений, например при удалении какого-либо его атрибута.

    В чем проблема сериализации Singleton?

    Проблема в том что после десериализации мы получим другой объект. Таким образом, сериализация дает возможность создать Singleton еще раз, что недопустимо. Существует два способа избежать этого:

    • явный запрет сериализации.
    • определение метода с сигнатурой (default/public/private/protected/) Object readResolve() throws ObjectStreamException , назначением которого станет возврат замещающего объекта вместо объекта, на котором он вызван.

    Какие существуют способы контроля за значениями десериализованного объекта

    Если есть необходимость выполнения контроля за значениями десериализованного объекта, то можно использовать интерфейс ObjectInputValidation с переопределением метода validateObject() .

    Так же существуют способы подписывания и шифрования, позволяющие убедиться, что данные не были изменены:

    Сериализация данных: тест производительности и описание применения

    Сериализация ( Serialize , в последующем «сохранение») – это процесс сохранения данных объекта во внешнем хранилище.
    Эта операция работает в паре с обратной – восстановлением данных, называемой десереализацией ( Deserealize , в последующем «восстановление»).

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

    В Java такие готовые механизмы существуют и, даже, в количестве более одного. Давайте разберемся, какие механизмы есть и какие возможности они предоставляют для программ на Kotlin .

    Само понятие сериализации никак не привязано к формату данных, в который будут сохранены данные, поэтому вне зависимости от того, какой результат будет получен – бинарный файл с собственной структурой, формат XML , JSON или даже текстовый файл – все это будет сериализацией.

    Сериализация

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

    В Java данные элементов одного объекта произвольно разбросаны по памяти JVM , поэтому даже если бы была возможность сохранения структуры объекта целиком, сохранить данные бы не получилось, поэтому единственно возможный путь в Java – это поэлементное сохранение данных элементарных типов из которых состоит объект.
    С одной стороны использовать сохранение объектов целиком, одной операцией, в Java невозможно, зато, благодаря наличию развитой RTTI , использование готовых средств для сохранения может быть реализовано очень легко.

    Многие классы потоков, такие как Writer или PrintStream предоставляют готовые возможности для сохранения элементарных типов данных, но использовать эти так же неудобно, как и в классических языках программирования из-за очень большого числа описаний, которые необходимо проделывать.
    Но, помимо работы с элементарными типами, в Java существует несколько разных типов готовых механизмов для сохранения данных классов и множество библиотек, реализующих работу с одними и теми же форматами, отличающихся друг от друга производительностью, объемом и предоставляемыми возможностями.

    Ниже будут рассмотрены типовые способы сохранения данных: встроенные в стандартную библиотеку Java, а так же сохранение в формате XML и JSON .

    Serializable

    Простейшая возможность, существующая в стандартной библиотеке Java – это сохранение и восстановление данных в полностью автоматическом режиме в бинарной форме. Для реализации этой возможности необходимо всего лишь указать у всех классов, данные которых должны автоматически сохраняться и восстанавливаться, интерфейс Serializable в качестве реализуемого. Это интерфейс «маркер», который не требует реализации ни одного метода. Он используется просто для обозначения того, что данные этого класса должны сохраняться и восстанавливаться.

    Пример использования

    Использование этого класса элементарно – одной операцией и не требует написания ни одной лишней буквы.

    В результате выполнения этого теста мы получим следующий вывод:

    Как уже видно из результата сохранение и восстановление объекта прошло успешно и, после восстановления, новый объект имеет точно такое же содержимое как сохраняемый.
    При выполнении программы был создан файл » out.bin » размером в 244 байта в бинарном формате. Описание формата можно найти во множестве источников, но, на мой взгляд, разбираться в нем не имеет никакого смысла, достаточно, чтобы его успешно понимали операции сохранения и восстановления.

    Особенности

    Если рассмотреть приведенный выше пример подробнее, то можно увидеть следующие особенности.

    • Были сохранены и восстановлены абсолютно все поля, даже те у которых указан тип доступа » private » и » protected «.
    • Обработаны были и поля, указанные как « val », т.е. неизменяемые по стандартам Kotlin .
    • Новый объект был создан, хотя конструктора без параметров у него нет, а существующий не вызывался.

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

    Для сохранения данных используется специальный поток ObjectOutputStream (и аналоги) для загрузки. Этот поток умеет работать с любыми типами данных и, в том числе, с объектами целиком, чем мы и воспользовались. Формируемые этим потоком данные содержат независимый набор блоков информации, поэтому никаких ограничений на его использование нет. Можно сохранять в один поток сколько угодно объектов или элементарных типов, главное, при восстановлении, прочитать их в обратном порядке.

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

    Классы для сохранения и восстановления данных являются потоками и их содержимое можно обернуть каким угодно классом, обрабатывающим потоки. Можно сжать сохраняемые данные, зашифровать, передавать по сети, сохранять в память, архив или любой внутренний контейнер.

    Возможности

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

    Любой уникальный объект сохраняется в поток только один раз. Если сохраняется несколько объектов, которые являются ссылками на один и тот же, то данные объекта будут сохранены только для одного из них, а для остальных будет записана только ссылка на уже сохраненный.
    При восстановлении данных объекты будут восстановлены так, что все ссылки будут восстановлены в том же виде, какой существовал в оригинальных объектах.

    При сохранении объектов автоматически отслеживаются их ссылки друг на друга и, при восстановлении, аналогичные объекты будут ссылаться на те же объекты. Т.е. если вы сохраняете объект «А» и объект «В» и при этом одним из полей объекта «В» является ссылка на сохраняемый объект «А» , то будет сохранено не две различные копии класса «А» , а только одна. При восстановлении полей новый объект «В» будет по прежнему ссылаться на объект «А» восстановленный из этого же потока, т.е. будет восстановлена связь меду объектами.
    Эта особенность позволяет абсолютно прозрачно сохранять связную иерархию объектов, ссылающихся друг на друга без разрушения связей и дублирования данных.

    Поддерживается сохранение классов типа «enum» с корректным их восстановлением.

    Поддерживается сохранение и восстановление любых объектов, которые имеет интерфейс-маркер Serializable. В частности, будут автоматически сохраняться все стандартные JDK коллекции, основанные на List, Set и Map т.к. все их реализации этот маркер имеют.
    Т.е. для того чтобы сохранить и восстановить все элементы списка или даже дерева не нужно писать никакого дополнительного кода, достаточно чтобы объекты были обозначены интерфейсом «Serializable».

    Для более точного управления процессом сохранения и восстановления данных можно использовать дополнительные механизмы.

    Контроль версии

    Т.к. сохранение данных объекта происходит полностью автоматически, то должен быть механизм, который контролирует совместимость сохраненных данных с текущей структурой объекта. Т.е. если мы добавили или удалили поле в классе, поменяли порядок полей или их тип, то при восстановлении данных они должны попасть на то место, для которого предназначены сохраненные данные.

    Такой механизм контроля совместимости существует. При сохранении библиотека автоматически вычисляет код для используемого класса, который описывает его состояние и, при восстановлении, проверяет совместим ли объект с теми данными, которые были сохранены для него ранее. Если к восстанавливаемому классу были добавлены поля или изменен их порядок, то восстановить данные из сохраненной копии можно, но если поля были удалены или у них изменился тип, то восстановить такие данные уже будет невозможно и, при попытке чтения такого объекта, будет выброшена ошибка.

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

    Это поле должно быть статической константой типа Long , описанной в классе.
    В случае Kotlin эта константа обязана быть описана с использованием аннотации @JvmStatic или модификатора const , иначе библиотека загрузки его не увидит.

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

    Для вычисления значения состояния класса можно воспользоваться утилитой «serialver» из поставки Java, но использовать ее неудобно, поэтому гораздо проще получить это значение программным путем. Для этого нужно в программе, которая использует нужный класс, вызвать метод для вычисления его состояния и полученное значение установить в поле serialVersionUID .

    Управление сохраняемыми данными

    Часто сохранять и восстанавливать нужно не все существующие у объекта данные, но только часть из них или восстанавливать их в формате, которые не соответствует их фактическому типу.

    Первой возможностью управления является механизм исключений.
    Для того, чтобы исключить какое-то поле из списка обрабатываемых его нужно пометить специальным типом «transient». В Java для этого используется специальное ключевое слово, а в Kotlin необходимо использовать специальную аннотацию.

    При обработке объектов этого класса библиотека сериализации не будет ни сохранять ни восстанавливать значения для поля » dbField «. Все остальные поля будут сохранены и восстановлены как обычно.
    Этот механизм удобно использовать в случаях, когда объекты поля, значения которых не имеет смысла или нельзя сохранять.
    Устанавливать значения полей, которые не будут обрабатываться автоматически, программист должен самостоятельно, после загрузки. Для этого можно воспользоваться методом » readResolve «, который описан ниже.

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

    Теперь наш пример сохраняет только два поля из трех доступных.

    Сохранение и восстановление данных вручную

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

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

    В этом примере поле класса теперь называется intFieldChanged , но в сохраняемых данных его имя по прежнему будет фигурировать как intField , что позволит загружать данные сохраненные со старым именем и сохранять их в таком виде, что старый класс будет способен их загрузить.

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

    Восстановление синглетонов

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

    Это поведение, так же, можно обеспечить.

    Для этого достаточно у класса, который должен обеспечивать уникальность, создать метод readResolve . Этот метод будет вызван после загрузки любого объекта этого класса и позволяет заменить его другим.

    Результат работы программы:

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

    Сохранение нестандартных классов, прокси

    В случае, если необходимо реализовать прозрачную работу с собственными классами, например с коллекциями в собственном формате или обеспечить подмену интерфейса класса (проксирование), то это тоже можно реализовать.
    Для этого нужно создать наследника для классов ObjectInputStream и ObjectOutputStream переопределив у них методы annotateClass или annotateProxyClass .

    Первый предназначен для обеспечения загрузки и сохранения неизвестных классов, а второй для проксирования интерфейса классов.

    Недостатки

    Библиотека, реализующая функционал интерфейса Serializable очень могучая, но она имеет ряд серьезных недостатков, которые в некоторых случаях могут являться принципиальным ограничением для ее использования.

    При сохранении и восстановлении данных объектов производится очень много действий.

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

    По моим измерениям, скорость работы этой библиотеки практически сопоставима со скоростью сохранения
    и восстановления данных в формат XML , т.е. довольно медленно. Библиотека Serializable несколько быстрее, но значительно уступает в скорости работы практически всем другим способам сохранения и восстановления данных.

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

    Формат сохраняемых данных – бинарный.

    Если когда-то может возникнуть задача, получить данные из такого файла на другом языке программирования или обеспечить возможность просмотра или редактирования его человеком, то такая задача может оказаться довольно сложно реализуемой.

    ВАЖНО: Serializable работает только с полями!

    Самый важный недостаток этого метода заключается в том, что он способен работать только с полями.

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

    Externalizable

    Второй метод, реализуемый штатными средствами Java – это интерфейс Externalizable .

    Все объекты реализующие этот интерфейс и их наследники могут быть сохранены в поток теми же классами ObjectInput и ObjectOutput что и реализующие интерфейс Serializable , но, в отличии от последнего, сохранение и восстановление данных объектов происходит полностью в ручном режиме.
    Интерфейс реализуется с помощью методов readExternal и writeExternal , на совести которых лежит сохранение и восстановление всех элементов класса.

    В отличии от Serializable , при реализации Externalizable , ответственность за загрузку данных предков класса лежит на программисте. Библиотека не будет читать и писать ничего автоматически, в поток будет сохранено только то, что явно вызвано в методах сохранения.

    У реализаций Serializable и Externalizable общая основа, поэтому при реализации последнего можно использовать практически все возможности первого.
    Можно сохранять и восстанавливать объекты целиком. В таком случае будет использоваться механизм сохранения и восстановления ссылок. У объектов можно реализовать функцию readResolve , которая будет играть точно ту же роль, можно сохранять те же самые коллекции с возможностью их автоматически загрузить.

    Принципиальное отличие работы кода Externalizable заключается в следующем:

    Не будут автоматически сохраняться данные предков объекта и, для их сохранения нужно явно описать действия по их сохранению.

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

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

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

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

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

    Особенности

    Основная особенность реализации интерфейса Externalizable заключается в том, что полный контроль над сохраняемыми данными, их форматом и порядком их следования находится в руках программиста.
    Этот метод сериализации, по удобству и эффективности, находится между прямой записью примитивов в поток и полной автоматизацией, предоставляемой библиотекой Serializable .

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

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

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

    К примеру, если сохранять абсолютно все данные вручную, то использование классов ObjectStream теряет всякий смысл, а связанные с их использованием накладные расходы и ограничения могут только мешать. Становится гораздо проще использовать возможности записи и чтения данных, предоставляемые любым бинарным потоком.

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

    Тестирование, сравнения

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

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

    Автоматически генерируемые случайные данные выглядят следующим образом:

    Имена методов, типов и исходных файлов генерируются из набора случайных имен.
    Имя исходного файла может отсутствовать, а текст <noname> и void обозначает синглетоны, обозначающие уникальные значения для имени файла или параметра.

    Данные, которые хранятся в классах только строкового типа, т.е. ни работу с массивами данных, ни с примитивными типами этот тест не использует. Тем не менее, я считаю, что полученные на нем результаты вполне отражают соотношение производительности различных методов друг с другом, хотя доля этого соотношения будет меняться в зависимости от типов данных и их количества.

    Используемые средства

    При тестировании использовались следующие способы сохранения и восстановления данных:

    SerialFull — Полностью автоматическая работа интерфейса Serializable .

    Extern+Ser — Реализация интерфейса Externalizable с кодом, в котором смешано ручное и автоматизированное сохранение данных.

    ExternFull — Реализация интерфейса Externalizable с полностью ручным сохранением данных.

    JsJsonMini — Библиотека «minimal-json», сохраняющая данные в формате JSON .
    В тесте использовалась библиотека minimal-json-0.9.4.jar , домашняя страница этого проекта находится тут: https://github.com/ralfstx/minimal-json.

    Библиотека fasterXML-jackson так же сохраняющая данные в формате JSON .
    В тесте использовалась библиотека версии 2.0.4 , домашняя страница этого проекта находится тут: https://github.com/FasterXML/jackson. С использованием этой библиотеки было реализовано два алгоритма работы с данными. Первый из них ( JsJackAnn ), полностью автоматический, управлялся только аннотациями, который называется в этой библиотеке annotations-databind подходом. Во втором ( JsJackSream ) было реализовано полностью ручной разбор дерева, называемого в этой библиотеке stream подходом.

    В таблице ниже приведены данные о каждом использованном средстве.

    Название Версия Источник Дополнительные библиотеки
    XML, Serializable, Externalizable Java 1.8 Штатная реализация Java Не требуются, входят в комплект поставки Java.
    minimal-json 0.9.4 https://github.com/ralfstx/minimal-json minimal-json-0.9.4.jar – 30Кб
    fasterXML-jackson 2.0.4 https://github.com/FasterXML/jackson jackson-core-2.0.4.jar – 194Кб, jackson-databind-2.0.4.jar – 847Кб, jackson-annotations-2.0.4.jar – 34Кб

    Процедура тестирования

    Для тестирования была реализована утилита, исходный тест которой доступен по ссылке, приведенной в конце.

    Эта утилита последовательно производит следующие действия:

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

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

    Результаты

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

    В этом тесте создавалось 100.000 случайных элементов, которые были записаны на диск и прочитаны с него 10 раз каждым из тестов. На тест было затрачено 1мин 28сек , из которых 25.9сек на накладные расходы утилиты тестирования.

    Место Имя Запись Лучш Худш Загрузка Лучш Худш Всего Лучш Худш Файл
    6 SerialFull 0:00:07.599 2,34 0:00:04.217 1,05 1,45 0:00:11.826 1,56 0,41 18Мб
    1 ExternFull 0:00:02.550 0,12 1,98 0:00:02.061 4,02 0:00:04.616 2,60 16Мб
    5 Extern+Ser 0:00:05.744 1,52 0,32 0:00:04.112 1,00 1,51 0:00:09.862 1,14 0,69 22.5Мб
    7 XMLw3c 0:00:06.278 1,76 0,21 0:00:10.337 4,02 0:00:16.620 2,60 32Мб
    4 JsJsonMini 0:00:04.678 1,05 0,62 0:00:04.614 1,24 1,24 0:00:09.302 1,02 0,79 25.9Мб
    3 JsJackAnn 0:00:02.776 0,22 1,74 0:00:02.431 0,18 3,25 0:00:05.215 0,13 2,19 25.9Мб
    2 JsJackSream 0:00:02.278 2,34 0:00:02.377 0,15 3,35 0:00:04.662 0,01 2,56 25.9Мб

    В этой таблице приведены результаты тестирования.

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

    Комментарии

    Serializable

    Этот механизм очень медленный.

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

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

    Externalizable

    Этот способ предсказуемо оказался самым скоростным при работе и генерирующим минимальные файлы данных.

    Однако скоростные и объемные показатели этого теста становятся заметными только при полном отказе от автоматизации для всех, часто используемых операций. Как только автоматизация используется более широко (тест Extern+Ser ) производительность программы стремительно падает, а объем файла данных растет. Причины этого явления описаны в главе ранее.

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

    Дополнительным минусом, который стоит отметить, является трудоемкость поиска ошибок рассогласования данных.

    Библиотека Java контролирует границы объектов и, если где-то происходит рассогласование процесса записи и чтения (например, поле сохраняется, но код для его загрузки описать забыли), то найти место проблемы очень сложно. Единственная ошибка, которую генерирует код Java – это EOF , что обозначает как достижение конца файла при чтении, так и попытку прочитать данные за пределами текущего типа.

    Использовать такой способ не имеет особого смысла т.к. он крайне многословен и не имеет особых преимуществ в объеме данных или скорости ни перед библиотеками JSON , ни перед ручнй реализаций собственного формата.

    minimal-json

    Эта библиотека является очень маленькой по объему, но работает медленно.

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

    Маленький размер – это, по сути, единственное достоинство этой библиотеки.

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

    fasterXML-jackson databind

    Библиотека очень большая, работает быстро, но требует больших усилий по адаптации данных для ее использования.

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

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

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

    Библиотека состоит из огромного количества сущностей с документацией на JavaDOC , т.е. понять влияние всех этих сущностей друг на друга практически невозможно, а найти нужную для реализации того или иного поведения можно только тотальным поиском в интернете.

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

    Использовать эту библиотеку имеет смысл в случаях, если:

    Существует множество несложных классов и часть сохраняемых свойств не существует в виде полей.

    Требуется быстрый сериализатор в формат JSON , а количество описаний в исходных текстах оказывается не очень большим.

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

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

    fasterXML-jackson stream

    Самый быстрый способ сохранить и загрузить данные в формате JSON с минимальными требованиями к объему памяти.

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

    Для использования этого способа достаточно библиотеки jackson-core , которая занимает 200Кб, что в 4 раз меньше объема для использования databind подхода.

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

    Этот способ является самым медленным и предъявляет самые большие требования к объему памяти.

    Дерево для всех объектов файла дынных не просто строится при загрузке данных, но при этом потребляет просто неприличный ее объем. При тестировании этот тест не смог выполниться с количеством элементов более 400.000 штук из-за того, что 5Гб выделенной для JVM памяти оказалось недостаточно.

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

    Использовать этот способ ни для больших исходных данных, ни для операций, где важна производительность, нельзя. Он работает медленно и потребляет много памяти.

    Сериализация простыми словами

    Много раз встречал эту «сериализацию» на разных ресурсах, часто связано с JSON.

    Объясните, пожалуйста, простыми словами, что такое сериализация, где и зачем ее применяют ?

    Сериализация — это преобразование объекта или дерева объектов в какой-либо формат с тем, чтобы потом эти объекты можно было восстановить из этого формата. Используется, например, для сохранения состояния программы (то есть, некоторых её объектов) между запусками. Или для передачи данных между различными экземплярами программы (или различными программами), например, по сети.

    Главная идея состоит в том, что сериализованный формат — набор байт или строка, которую можно легко сохранить на диск или передать другому процессу или, например, по сети, в отличие от самого объекта. А значит, задача сохранения объекта/группы объектов при этом сводится к простой задаче сохранения набора байт или строки.

    JSON — один из популярных форматов для сериализации, он текстовый, легковесный и легко читается человеком.

    Пример: если у вас есть класс

    Объект этого класса в сериализованной форме может иметь вид

    Саму сериализацию (и десериализацию) можно производить вручную, или пользоваться соответствующими библиотеками/фреймворками.

    Сериализация в программировании

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

    Программирование осуществляется с помощью специальных языков. Каждый обладает ключевыми особенностями, преимуществами, недостатками и нюансами. Основная масса современных ЯП универсальны – могут применяться как для веб-коддинга, так и для игрового софта. Согласно Google, огромным спросом пользуются языки СИ-семейства, Java, а также Python.

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

    Основные термины и понятия

    Разработка – процесс, требующий определенных общих знаний. В программировании есть общие понятия, о которых необходимо помнить каждому юзеру, желающему создать собственное программное обеспечение:

    1. Алгоритм – инструкции, правила и указания, направленные на решение той или ной задачи.
    2. API – правила, принципы, протоколы и процедуры, направленные на помощь в создании программных приложений. Такой интерфейс способствует упрощению контактирования со сторонними службами и утилитами.
    3. Аргументы – значения, которые передаются в функции или команды.
    4. Ошибка – дефект или непредвиденный крах, приводящий к неисправностям в написанном коде.
    5. Символ – элементарная единица записи (отображения данных), которая выражена одной буквенной или цифирной записи.
    6. Объекты – связанные переменные, константы, а также иные структурные данные. Они будут выбираться и проходить совместную обработку при исполнении приложения.
    7. Объектно-ориентированное программирование – способ написания ПО, в основе которого заложены объекты и данные. Действия и логика присутствуют, но уходят на второй план.
    8. Класс – набор связанных между собой объектов, наделенных одними и теми же свойствами. Предназначается для гибкости программирования.
    9. Константа – неизменное значение. Оно остается одинаковым на протяжении всего цикла приложения.
    10. Тип данных – классификация определенной разновидности типа.
    11. Массив – множество схожих типов данных, подвергшихся предварительной группировке.
    12. Итерация – один проход через определенный набор операций в коде.
    13. Ключевое слово – зарезервированное языком программирования или утилитой слово. Необходимо для выполнения тех или иных задач, реализации команд. Ключевики не могут выступать в виде названий переменных.
    14. Переменная – единица хранения информации в памяти устройства, которая имеет собственное уникальное имя.
    15. Операнд – то, чем можно управлять через операторы.
    16. Оператор – объект, который при помощи операндов управляет другими составляющими кода в заданном выражении.
    17. Указатель – переменная, хранящая в себе адрес места в памяти.

    Это – база, без которой невозможно успешно коддить, а также рассматривать сериализации и десериализации. Отыскать подобные определения успешно удастся через Google. Также стоит учесть, что у каждого ЯП есть «специфические» термины, используемые для конкретного способа написания ПО.

    Актуальность темы

    Сериализация объектов в языках программирования (будут рассмотрены примеры из Java и Python) – механизм, который помогает разрабам создать уникальное ПО, которое умеет «запоминать» собственное состояние.

    Пример – это компьютерная игра. Даже на приставках из 90-х были такие операции как «сохранение» и «загрузка». Но не везде. Из-за этого возникали определенности трудности. Особенно если речь заходила не просто об играх, а о «серьезном» программном обеспечении.

    Сохранение и загрузка игры в обычном понимании (и, если посмотреть Google) – это продолжение с того места, где клиент закончил в прошлый раз. Для этого будет создана специальная «контрольная точка». Она задействуется при следующей загрузке ПО. Так процесс звучит в «обыденном» смысле.

    В программировании ситуация обстоит аналогичным образом. Процесс можно описать следующим образом:

    1. Пользователь работает с утилитой и производит какие-то изменения.
    2. Когда нужно – пытается сохранить состояние программы.
    3. Информацию о приложении требуется как-то записывать, чтобы в будущем можно было произвести восстановление прогресса. Для этого нужны специальные механизмы.

    Именно к этим операциям будет обращаться пользователь, когда ему требуется загрузить или выгрузить то или иное состояние контента. Манипуляции способствуют продолжению работы клиента.

    Сериализация и десериализация – общее

    Сериализация имеющихся объектов – это процесс сохранения состояния объекта в последовательность байт. Механизм, который позволят «записывать» полученный прогресс для будущей выгрузки.

    Десериализация – восстановление объектов из байт, сохранение которых было произведено ранее. Процедура выгрузки «зафиксированной» информации пользователем.

    В Java и Python любые объекты, согласно Google, проходят через преобразование в байт последовательность. Это важный процесс, без которого существование современного ПО оказалось бы невозможным.

    Для чего требуется

    Программные коды не могут существовать сами по себе. Они обычно взаимодействуют друг с другом, а также обмениваются данными. Байтовый формат – это удобное и эффективное средство хранение информации в памяти. Он позволяет превращать объекты (пример – класс SaveGame – сохраненная игра) в последовательность байт. Далее – передать их через интернет на другое устройство (компьютер, смартфон, планшет), а после превратить соответствующие bytes в объект Java или Python.

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

    Сериализация соответствующих объектов очень удобна, когда речь идет о работе со сложными компонентами при коддинге. Это способствует созданию качественного ПО.

    В Java

    Стоит рассмотреть описанные операции на примере двух самых популярных ЯП. Начать лучше с Java, который является более функциональным и универсальным. С его помощью могут быть написаны даже сложные игры и «офисные» приложения.

    В Java, согласно Google, сервиализация возможна лишь относительно объектов, которые используют интерфейс Serializable. Он:

    • не служит для определения тех или иных методов;
    • выступает в качестве своеобразного системного указателя;
    • «ссылается» на то, что объект, который его реализовывает, может быть сериализован.

    Сериализация в Java – это своеобразное представление информации в байтах ссылками через специальный класс.

    Класс ObjectOutputStream

    Для того, чтобы сериализуемый набор объектов был «собран» в поток, необходимо применить class Object Output Stream (пишется слитно). Он отвечает за потоковую запись информации.

    Чтобы создать подобный объект, согласно Google, требуется использовать такую форму представления в конструкторе: ObjectOutputStream (OutputStream out).

    Запись электронных материалов упомянутым классом ведется за счет определенных методов. Они включают в себя следующие вариации:

    • void close() – закрытие потока;
    • void flush() – очистка буфера и сброс содержимого в выходной поток;
    • write(byte[] buf) – запись в поток массив байтов;
    • write (int val) – преобразование один младший байт из val;
    • writeBoolean(int val) – работа с булевым значением;
    • writeByte(int val) – сохранение в поток один младший байт из val;
    • writeChar(int val) – значение типа char, которое представлено целочисленным;
    • writeDouble – значение типа double;
    • writeLong – тип long;
    • writeInt – целочисленный тип данных int;
    • writeShort – запись типа short;
    • writeUTF(String str) – записывает в поток строку, которая идет в кодировке UTF-8;
    • writeObject(Object obj) – запись в поток отдельного объекта.

    Согласно Google, все предложенные методы – это информация, с которой допускается провести сериализацию в Java.

    Пример

    Чтобы лучше понимать соответствующее направление, стоит рассмотреть наглядный пример. Вот файл с кодом, в который хотим сохранить один объект класса Person:

    Он наглядно отражает сериализацию объектов в программировании на примере Джавы. Подобный приложения можно с легкостью обнаружить в Google.

    Десериализация

    В Джаве есть обратный от упомянутого ранее класс. Это – ObjectInputStream. Он позволяет провести чтение электронных материалов, которые были serialized ранее. Принимает ссылку в имеющемся конструкторе на поток ввода: ObjectInputStream(InputStream in).

    А вот методы, которые использует соответствующий класс:

    Из ранее созданного класса, прошедшего serializing, можно извлечь электронные материалы:

    А теперь можно объединить эти файлы (serialization и deserialization) в единый документ и вывести по полям списка объектов:

    Исключение

    Изначально при сериализации рассматриваемых объектов в ЯП, согласно Google, будут использоваться все переменные object. Но бывают случаи, когда некоторые поля требуется исключить. Это можно сделать при помощи модификатора.

    В соответствующей ситуации предстоит запомнить, что:

    1. Название необходимого модификатора – это transient.
    2. Если взять наглядный пример с Person, можно исключить из него переменные height.
    3. Убрать разрешается несколько элементов. Пример – дополнительно из Person исключим married.

    Выше – сериализация заданных объектов с исключениями. Подобных примеров в Google тоже немало. И все они способны разъяснить процесс serialize даже новичкам.

    В Питоне

    В случае с Питоном ситуация обстоит несколько иначе. Далее будут приведены примеры со средой разработки IntelJ IDEA со стандартным расширением от JetBrains.

    Чтобы успешно проверить сериализацию в Python, необходимо произвести импорт модулей. А еще – создать таблицу информации, с которой будет осуществляться дальнейшая работа.

    Инструменты Питона

    У Питона не one способ реализации рассматриваемой операции. Их несколько:

    • NumPy. Он будет вести запись массива в файл в качестве одномерного. Реализовывается через метод np/ndarray.tolist().
    • Google также указывает на то, что есть Pandas, который будет использовать похожий интерфейс. Здесь предусмотрен более широкий функционал. Запись – через метод pd.DataFrame.to_csv(), чтение – pd.read_csv().
    • Согласно Google, можно реализовать поставленную задачу через JSON. Для этого требуются функции load и dump.
    • Pickle – если верить Google, представлен стандартной библиотекой, похожей на JSON.

    Все это поможет лучше разобраться с соответствующей тематикой. В Google полно разнообразной документации как по Питону, так и по Джаве. И вот один из примеров.

    Хотите освоить современную IT-специальность? Огромный выбор курсов по востребованным IT-направлениям есть в Otus!

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

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