JavaRush /Java блог /Архив info.javarush /Сериализация как она есть. Часть 2
articles
15 уровень

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

Статья из группы Архив info.javarush

Производительность

Как я уже говорил, стандартная сериализация работает через Reflection API. Что означает, что для сериализации берется класс сериализуемого объекта, у него берется список полей, по всем полям в цикле проверяются различные условия ( transient или нет, если объект, то Externalizable или Serializable), значения пишутся в поток, причем достаются из полей тоже через reflection... В общем, ситуация ясна. В противоположность этому методу, вся процедура при использовании расширенной сериализации контролируется самим разработчиком. Осталось выяснить, какие преимущества это дает по скорости. Итак, условия теста. Объект произвольной структуры. Два варианта – один Serializable, второй Externalizable. Некоторое количество объектов обоих вариантов инициализируется произвольными (идентичными для каждой пары объектов) данными, после чего помещается в контейнер. Контейнер тоже в одном случае Serializable, в другом Externalizable. Далее контейнеры будут сериализованы и десериализованы с замерами времени. Полный код теста вместе с build-файлом для ant можно найти тут – serialization.zip (скачать можно с сайта-первоисточника). В тексте я буду приводить только отрывки. Сериализуемый объект содержит следующий набор полей: private int fieldInt; private boolean fieldBoolean; private long fieldLong; private float fieldFloat; private double fieldDouble; private String fieldString; Тест содержит три реализации Externalizable контейнеров. Первая из них, ContainerExt1, простейшая. Это просто сериализация содержащего объекты java.util.List: public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(items); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { items = (List)in.readObject(); } Вторая реализация, ContainerExt2, сериализует последовательно все имеющиеся объекты, предваряя их количеством объектов: public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(items.size()); for(Externalizable ext : items) out.writeObject(ext); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { int count = in.readInt(); for(int i=0; i Третья реализация, ContainerExt3, использует externalizable-методы объектов: public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(items.size()); for(Externalizable ext : items) ext.writeExternal(out); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { int count = in.readInt(); for(int i=0; i Запускается тест с помощью команды ant (поскольку задача run запускается по умолчанию). В build-файле задано количество создаваемых объектов – 100000. Другое количество может быть задано с помощью параметра командной строки -Dobjcount=. Итак, каковы результаты выполнения теста? На 100000 создаваемых объектов (результаты могут незначительно отличаться от запуска к запуску): Creating 100000 objects Serializable: written in 3516ms, readed in 3235 Externalizable1: written in 4046ms, readed in 3234 Externalizable2: written in 3875ms, readed in 2985 Externalizable3: written in 235ms, readed in 297 И размеры сериализованных данных (размеры файлов на диске): cont.ser 5 547 955 contExt1.ser 5 747 884 contExt2.ser 5 747 846 contExt3.ser 4 871 461 Что мы видим? Первый способ реализации Externalizable даже несколько хуже стандартной сериализации. Сериализация занимает немного больше времени, десериализация сравнима. Размеры файлов тоже немного в пользу стандартной сериализации. Вывод – простейшая сериализация контейнера преимуществ не дает: +15% при сериализации, десериализация отличается на доли процента, причем как в одну, так и в другую сторону. Второй способ реализации Externalizable по характеристикам практически идентичен первому. Чуть быстрее сериализация, но все равно проигрывает стандартной, десериализация чуть выигрывает. Размер файла практически идентичен первому способу (разница – 38 байт). Выигрыша по сравнению со стандартной сериализацией нет – +10% при сериализации, -8% при десериализации. Третий способ реализации Externalizable. Вот тут есть на что посмотреть! Сериализация быстрее в 15 раз! Естественно, плюс-минус, но тем не менее – разница на порядок! Десериализация быстрее практически в 11 раз! Разница тоже на порядок! Опять же плюс-минус, но мне не удавалось получить разницу меньше, нежели в 5 раз. Ну и разница в размере файла -13%. Как маленькое, но приятное дополнение. Думаю, комментарии излишни. Получаемые от грамотной реализации Externalizable преимущества в скорости с лихвой компенсируют затраты на эту самую реализацию. Грамотной – в смысле, целиком и полностью реализованной самостоятельно, без использования имеющихся механизмов сериализации целых объектов (в основном это методы writeObject/readObject). Использование же имеющихся механизмов и/или смешивание со стандартной сериализацией способно свести скоростные преимущества Externalizable на нет. Однако есть и ...

Обратная сторона медали

И прежде всего это нарушение целостности графа. Поскольку протокол сериализации не используется – контроль целостности остается на самом разработчике. И об этом следует помнить, ибо в некоторых случаях можно легко убить все преимущества. Если, к примеру, необходимо сериализовать очень много экземпляров класса A, каждый из которых ссылается на единственный экземпляр класса B, то при неумелом использовании Externalizable может получиться так, что экземпляр B будет сериализован по разу на каждый экземпляр A, что даст потерю как в скорости, так и в объеме сериализованных данных. А при десериализации мы вообще получим кучу экземпляров B вместо одного! Что намного хуже. Поэтому, да и не только, Externalizable следует использовать обдуманно. Как, впрочем, и любую другую возможность. Если необходимо сериализовать достаточно сложные графы – пожалуй, лучше все-таки воспользоваться имеющимися механизмами. Если же объемы данных большие, но сложность невелика – можно немного поработать и получить солидный выигрыш в скорости. В любом случае лучше написать небольшой прототип и уже на нем оценивать реальную скорость и сложность реализации целостности. Перейдем к следующему вопросу, связанному с сериализацией.

Безопасность данных

Есть такое правило: проверять входящие данные (входные параметры функций и т.п.) на "правильность" – соответствие определенным требованиям. Причем это не столько правило хорошего тона, сколько правило выживания приложения. Ибо если этого не сделать, то при передаче неверных параметров в лучшем случае (действительно – в лучшем!) приложение просто "упадет". В худшем случае оно тихо примет предложенные данные и может нанести значительно больший урон. Про это правило худо-бедно, но помнят. Однако конструкторы и открытые методы – не единственный способ поставки данных объекту. Точно так же объект может быть создан с помощью десериализации. И вот тут о контроле внутреннего состояния полученного объекта, как правило, забывают. Между тем, создать поток для получения из него объекта с неверным внутренним состоянием не легко, а очень легко. Пример номер один. Объект с двумя полями типа java.util.Date. Одно поле – начало интервала времени, другое – конец. Следовательно, между ними должно существовать определенное соотношение (конец должен быть не раньше начала). Однако любой человек, знающий байткод, сумеет отредактировать сериализованный объект так, что после десериализации конец интервала будет раньше начала. К чему приведет появление в системе такого объекта – предугадать сложно. В любом случае, ничего хорошего ждать не приходится. Потому, примите для себя...
Правило 1. После десериализации объекта необходимо проверить его внутреннее состояние (инварианты) на правильность, точно так же, как и при создании с помощью конструктора. Если объект не прошел такую проверку, необходимо инициировать исключение java.io.InvalidObjectException.
Пример номер два. Объект класса A содержит в себе private-поле типа java.util.Date. Для изменения снаружи объекта это поле недоступно. Однако возможна следующая операция: к потоку дописывается некоторая информация. Потом, после десериализации из этого потока объекта класса A производится десериализация еще одного объекта, но уже типа Date. Как мы уже видели в примере ранее, можно создать такой поток (в примере он создавался легально), что при десериализации этот второй объект в действительности будет лишь ссылкой на экземпляр Date, казалось бы так надежно спрятанный внутри объекта класса A. Соответственно, с этим экземпляром можно делать все, что заблагорасудится.
Не буду вдаваться в подробности. Описание этого приема есть в книге Джошуа Блох. Java. Эффективное программирование, в статье 56. Скажу только, что достаточно к потоку дописать 5 байт, чтобы добиться желаемого.
Чтобы этого избежать, необходимо следовать следующему правилу:
Правило 2. Если в составе класса A присутствуют объекты, которые не должны быть доступными для изменения извне, то при десериализации экземпляра класса A необходимо вместо этих объектов создать и сохранить их копии.
Приведенные выше примеры показывают возможные "дыры" в безопасности. Следование упомянутым правилам, разумеется, не спасает от проблем, но может существенно снизить их количество. Советую по этому поводу почитать книгу Джошуа Блох. Java. Эффективное программирование, статью 56. Ну и последняя тема, которой я хотел бы коснуться –

Сериализация объектов Singleton

Тех, кто не в курсе, что такое Singleton, отсылаю к отдельной статье. В чем проблема сериализации Singleton-ов? А проблема в уже упомянутом мной факте – после десериализации мы получим другой объект. Это видно в результатах первого из тестов в этой статье – ссылки на исходный и десериализованный объекты не совпадают. Таким образом, сериализация дает возможность создать Singleton еще раз, что нам совсем не нужно. Можно, конечно, запретить сериализовать Singleton-ы, но это, фактически, уход от проблемы, а не ее решение. Решение же заключается в следующем. В классе определяется метод со следующей сигнатурой ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException Модификатор доступа может быть private, protected и по умолчанию (default). Можно, наверное, сделать его и public, но смысла я в этом не вижу. Назначение этого метода – возвращать замещающий объект вместо объекта, на котором он вызван. Приведу простой пример: public class Answer implements Serializable{ private static final String STR_YES = "Yes"; private static final String STR_NO = "No"; public static final Answer YES = new Answer(STR_YES); public static final Answer NO = new Answer(STR_NO); private String answer = null; private Answer(String answer){ this.answer = answer; } private Object readResolve() throws ObjectStreamException{ if (STR_YES.equals(answer)) return YES; if (STR_NO.equals(answer)) return NO; throw new InvalidObjectException("Unknown value: " + answer); } } Класс, приведенный выше – простейший перечислимый тип. Всего два значения – Answer.YES и Answer.NO. Соответственно, именно эти два значения и должны фигурировать после десериализации. Что делается в методе readResolve? Он вызывается на десериализованном объекте. И возвращать он должен уже существующий экземпляр класса, соответствующий внутреннему состоянию десериализованного объекта. В данном примере – проверяется значение поля answer. Если объект, соответствующий внутреннему состоянию, не найден... На мой взгляд, это зависит от ситуации. В приведенном примере стоит инициировать исключение. Возможно, в каких-то ситуациях будет полезно вернуть this. Примером этого, например, является реализация java.util.logging.Level. Существует и обратный метод – writeReplace, который, как вы, наверное, уже догадались, позволяет выдать замещающий объект вместо текущего, для сериализации. Мне, честно сказать, трудно представить себе ситуации, в которых это может понадобиться. Хотя в недрах кода Sun он как-то используется. Оба метода, как readResolve, так и writeReplace, вызываются при использовании стандартных средств сериализации (методов readObject и writeObject), вне зависимости от того, объявлен ли сериализуемый класс как Serializable или Externalizable. Самое интересное, что, похоже, из этих методов можно возвращать не только экземпляр класса, в котором этот метод определен, но и экземпляр другого класса. Я видел подобные примеры в глубинах библиотек Sun, во всяком случае, для writeReplace – точно видел. Но по каким принципам можно это делать – не берусь пока судить. Вообще, советую интересующимся просмотреть исходники J2SE 5.0, причем полные. Они доступны по лицензии JRL. Там есть много интересных примеров использования этих методов. Исходники можно взять тут – http://java.sun.com/j2se/jrl_download.html. Правда, требуется регистрация, но она, естественно, бесплатна. Отдельно хочу коснуться сериализации перечислений (enum), появившихся в Java 5.0. Поскольку при сериализации в поток пишется имя элемента и его порядковый номер в определении в классе, можно было бы ожидать проблем при десериализации в случае изменения порядкового номера (что может случиться очень легко – достаточно поменять элементы местами). Однако, к счастью, таких проблем нет. Десериализация объектов типа enum контролируется для обеспечения соответствия десериализуемых экземпляров уже имеющимся у виртуальной машины. Фактически, это то, что делает обычно метод readResolve, но реализовано где-то существенно глубже. Сопоставление объектов осуществляется по имени. Разработчикам версии 5.0 – респект! * * * Наверное, на текущий момент это все, что я хотел рассказать о сериализации. Думаю, теперь она не кажется такой простой, какой казалась до прочтения этой статьи. И хорошо. Пребывание в блаженном неведении к добру не приводит. Ссылка на первоисточник: http://www.skipy.ru/technics/serialization.html
Комментарии (12)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Роман Кончалов Уровень 28 Expert
1 октября 2021
Полезная информация, но без форматирования очень туго
PaiMei in J# Уровень 35
28 апреля 2021
Метод readResolve вызывается после readObject. Объект, который возвращает метод, заменяет this объект, возвращенный пользователю ObjectInputStream.readObject и любые дальнейшие обратные ссылки на объект в потоке. Мы можем использовать метод readResolve для замены десериализованного объекта экземпляром singleton. ПыСы: Дай - ка угадаю, наверняка ты, мой читающий этот комментарий друг, сейчас решаешь задачу 4 лекции 20 уровня?) Тогда надеюсь это тебе поможет)
Burakov Vladimir Уровень 41
19 января 2020
Может кто подсказать в каком классе искать метод "private Object readResolve()" в документации оракла в разделе "ObjectInputstream" нет такого метода.
al Уровень 24
12 января 2020
Первая часть статьи: https://javarush.com/groups/posts/1406-serializacija-kak-ona-estjh-chastjh-1 Обе части интересные, обозначено много нюансов. Первая часть вообще manual к последнему блоку задачь 20 уровня. Обоим авторам респект. Многое тема сериализации стала понятней.
Nordis Уровень 28 Expert
1 сентября 2019
Статья 2015 года . И того 2195 просмотров статьи , и 4 комментария (не включая моего) . Один из которых жалуется что режет глаза . И один с благодарностью . Я в теме Сериализации не слишком то шарю , сложноватая тема . И по кол-ву комментов , я смотрю я не один такой. Так как после понятной темы , много вопросов задают люди , а что если так или так ... Начинаются , споры , баталии . А тут тихо :) Ах да , по самой статье . Честно скажу мало что понятно . Но , спасибо , объёмно .
11 июля 2019
И лин к бы на первую часть статьи тоже бы не помешал(
Gwinblade Уровень 33
9 июля 2019
Информация была скопирована с этого сайта http://www.skipy.ru/technics/serialization.html
Alanser Уровень 26
9 июля 2019
Блин, форматируйте эту статью, глаза режет читать.