JavaRush /Blog Java /Random-PL /Serializacja taka jaka jest. Część 2
articles
Poziom 15

Serializacja taka jaka jest. Część 2

Opublikowano w grupie Random-PL

Wydajność

Jak już powiedziałem, standardowa serializacja działa poprzez API Reflection. Oznacza to, że do serializacji pobierana jest klasa serializowanego obiektu, pobierana jest z niej lista pól, dla wszystkich pól w pętli sprawdzane są różne warunki ( przejściowe lub nie, jeśli obiekt, to Externalizable lub Serializable ), wartości są zapisywane do strumienia, a także są pobierane z pól poprzez odbicie ... Generalnie sytuacja jest jasna. W przeciwieństwie do tej metody całą procedurę przy zastosowaniu rozszerzonej serializacji kontroluje sam programista. Czas pokaże, jakie korzyści daje to pod względem szybkości. A więc warunki testu. Obiekt o dowolnej strukturze. Dwie opcje — jedna Możliwość serializacji , druga Możliwość zewnętrznej obsługi . Pewna liczba obiektów obu opcji jest inicjowana dowolnymi danymi (identycznymi dla każdej pary obiektów), a następnie umieszczana w kontenerze. Kontener można także serializować w jednym przypadku i eksternalizować w innym . Następnie kontenery zostaną serializowane i deserializowane z pomiarami czasu. Pełny kod testowy wraz z plikiem kompilacji anta można znaleźć tutaj – serialization.zip (można go pobrać ze strony źródłowej). W tekście przytoczę jedynie fragmenty. Obiekt możliwy do serializacji zawiera następujący zestaw pól: private int fieldInt; private boolean fieldBoolean; private long fieldLong; private float fieldFloat; private double fieldDouble; private String fieldString; Test zawiera trzy implementacje kontenerów zewnętrznych . Pierwszy, ContainerExt1 , jest najprostszy. Jest to po prostu serializacja zawierających obiekty java.util.List : Druga implementacja, ContainerExt2 , serializuje wszystkie istniejące obiekty sekwencyjnie, poprzedzając je liczbą obiektów:public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(items); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { items = (List )in.readObject(); } 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-методы obiektов: 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-файле задано количество создаваемых obiektов – 100000. Другое количество может быть задано с помощью параметра командной строки -Dobjcount= . Итак, Jakовы результаты выполнения теста? На 100000 создаваемых obiektов (результаты могут незначительно отличаться от запуска к запуску): 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 Co мы видим? Первый способ реализации Externalizable даже несколько хуже стандартной сериализации. Сериализация занимает немного больше времени, десериализация сравнима. Размеры файлов тоже немного в пользу стандартной сериализации. Вывод – простейшая сериализация контейнера преимуществ не дает: +15% при сериализации, десериализация отличается на доли процента, причем Jak в одну, так и в другую сторону. Второй способ реализации Externalizable по характеристикам практически идентичен первому. Чуть быстрее сериализация, но все равно проигрывает стандартной, десериализация чуть выигрывает. Размер plik практически идентичен первому способу (разница – 38 bajt). Выигрыша по сравнению со стандартной сериализацией нет – +10% при сериализации, -8% при десериализации. Третий способ реализации Externalizable. Вот тут есть на что посмотреть! Сериализация быстрее в 15 раз! Естественно, плюс-минус, но тем не менее – разница на порядок! Десериализация быстрее практически в 11 раз! Różnica тоже на порядок! Опять же плюс-минус, но мне не удавалось получить разницу меньше, нежели в 5 раз. Ну и разница в размере plik -13%. Как маленькое, но приятное дополнение. Думаю, комментарии излишни. Получаемые от грамотной реализации Externalizable преимущества в скорости с лихвой компенсируют затраты на эту самую реализацию. Грамотной – в смысле, целиком и w pełni реализованной самостоятельно, без использования имеющихся механизмов сериализации целых obiektов (в основном это методы writeObject/readObject). Использование же имеющихся механизмов и/Lub смешивание со стандартной сериализацией способно свести скоростные преимущества Externalizable на нет. Однако есть и ...

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

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

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

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

Сериализация obiektов Singleton

Тех, кто не в курсе, что такое Singleton, отсылаю к отдельной статье. В чем проблема сериализации Singleton-ов? А проблема в уже упомянутом мной факте – после десериализации мы получим другой obiekt. Это видно в результатах первого из тестов в этой статье – ссылки на исходный и десериализованный obiektы не совпадают. Таким образом, сериализация дает возможность создать Singleton еще раз, что нам совсем не нужно. Можно, конечно, запретить сериализовать Singleton-ы, но это, фактически, уход от проблемы, а не ее решение. Решение же заключается в следующем. В классе определяется метод со следующей сигнатурой ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException Модификатор доступа может быть private, protected и по умолчанию (default). Можно, наверное, сделать его и public, но смысла я в этом не вижу. Наoznaczający этого метода – возвращать замещающий obiekt zamiast obiektа, на котором он вызван. Приведу простой пример: 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. Соответственно, именно эти два значения и должны фигурировать после десериализации. Co делается в методе readResolve? Он вызывается на десериализованном obiektе. И возвращать он должен уже существующий экземпляр класса, соответствующий внутреннему состоянию десериализованного obiektа. В данном примере – проверяется oznaczający поля answer. Если obiekt, соответствующий внутреннему состоянию, не найден... На мой взгляд, это зависит от ситуации. В приведенном примере стоит инициировать исключение. Возможно, в Jakих-то ситуациях будет полезно вернуть this. Примером этого, например, является реализация java.util.logging.Level. Существует и обратный метод – writeReplace, который, Jak вы, наверное, уже догадались, позволяет выдать замещающий obiekt zamiast текущего, для сериализации. Мне, честно сказать, трудно представить себе ситуации, в которых это может понадобиться. Хотя в недрах kodа Sun он Jak-то используется. Оба метода, Jak readResolve, так и writeReplace, вызываются при использовании стандартных средств сериализации (методов readObject и writeObject), вне зависимости от того, объявлен ли сериализуемый класс Jak Serializable Lub Externalizable. Самое интересное, что, похоже, из этих методов можно возвращать не только экземпляр класса, в котором этот метод определен, но и экземпляр другого класса. Я видел подобные примеры в глубинах библиотек Sun, во всяком случае, для writeReplace – точно видел. Но по Jakим принципам можно это делать – не берусь пока судить. Вообще, советую интересующимся просмотреть исходники J2SE 5.0, причем полные. Они доступны по лицензии JRL. Там есть много интересных примеров использования этих методов. Исходники можно взять тут – http://java.sun.com/j2se/jrl_download.html. Правда, требуется регистрация, но она, естественно, бесплатна. Отдельно хочу коснуться сериализации перечислений (enum), появившихся в Java 5.0. Поскольку при сериализации в поток пишется Nazwa element и его порядковый номер в определении в классе, можно было бы ожидать проблем при десериализации в случае изменения порядкового номера (что может случиться очень легко – достаточно поменять элементы местами). Однако, к счастью, таких проблем нет. Десериализация obiektов типа enum контролируется для обеспечения соответствия десериализуемых экземпляров уже имеющимся у виртуальной машины. Фактически, это то, что делает обычно метод readResolve, но реализовано где-то существенно глубже. Сопоставление obiektов осуществляется по имени. Разработчикам версии 5.0 – респект! * * * Наверное, на текущий момент это все, что я хотел рассказать о сериализации. Думаю, теперь она не кажется такой простой, Jakой казалась до прочтения этой статьи. И хорошо. Пребывание в блаженном неведении к добру не приводит. Ссылка на первоисточник: http://www.skipy.ru/technics/serialization.html
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION