JavaRush /Blog Java /Random-ES /Serialización tal como está. Parte 2
articles
Nivel 15

Serialización tal como está. Parte 2

Publicado en el grupo Random-ES

Actuación

Como ya dije, la serialización estándar funciona a través de la API Reflection. Lo que significa que para la serialización, se toma la clase del objeto que se está serializando, se toma una lista de campos de ella, se verifican varias condiciones para todos los campos en el bucle ( transitorio o no, si es un objeto, entonces externalizable o serializable ), los valores se escriben en la secuencia y también se recuperan de los campos mediante reflexión ... En general, la situación es clara. A diferencia de este método, todo el procedimiento cuando se utiliza la serialización extendida lo controla el propio desarrollador. Queda por ver qué ventajas aporta esto en términos de velocidad. Entonces, las condiciones de prueba. Un objeto de estructura arbitraria. Dos opciones: una serializable y la segunda externalizable . Un cierto número de objetos de ambas opciones se inicializan con datos arbitrarios (idénticos para cada par de objetos) y luego se colocan en un contenedor. El contenedor también es serializable en un caso y externalizable en otro . A continuación, los contenedores se serializarán y deserializarán con mediciones de tiempo. El código de prueba completo junto con el archivo de compilación de ant se puede encontrar aquí: serialization.zip (puede descargarlo desde el sitio fuente). En el texto daré sólo extractos. El objeto serializable contiene el siguiente conjunto de campos: private int fieldInt; private boolean fieldBoolean; private long fieldLong; private float fieldFloat; private double fieldDouble; private String fieldString; La prueba contiene tres implementaciones de contenedores externalizables . El primero, ContainerExt1 , es el más sencillo. Esto es simplemente una serialización de los objetos java.util.List que los contienen : la segunda implementación, ContainerExt2 , serializa todos los objetos existentes de forma secuencial, anteponiéndolos con el número de objetos: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-методы un objetoов: 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-файле задано количество создаваемых un objetoов – 100000. Другое количество может быть задано с помощью параметра командной строки -Dobjcount= . Итак, Cómoовы результаты выполнения теста? На 100000 создаваемых un objetoов (результаты могут незначительно отличаться от запуска к запуску): 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 Qué мы видим? Первый способ реализации Externalizable даже несколько хуже стандартной сериализации. Сериализация занимает немного больше времени, десериализация сравнима. Размеры файлов тоже немного в пользу стандартной сериализации. Вывод – простейшая сериализация контейнера преимуществ не дает: +15% при сериализации, десериализация отличается на доли процента, причем Cómo в одну, так и в другую сторону. Второй способ реализации Externalizable по характеристикам практически идентичен первому. Чуть быстрее сериализация, но все равно проигрывает стандартной, десериализация чуть выигрывает. Размер archivo практически идентичен первому способу (разница – 38 byte). Выигрыша по сравнению со стандартной сериализацией нет – +10% при сериализации, -8% при десериализации. Третий способ реализации Externalizable. Вот тут есть на что посмотреть! Сериализация быстрее в 15 раз! Естественно, плюс-минус, но тем не менее – разница на порядок! Десериализация быстрее практически в 11 раз! Diferencia тоже на порядок! Опять же плюс-минус, но мне не удавалось получить разницу меньше, нежели в 5 раз. Ну и разница в размере archivo -13%. Как маленькое, но приятное дополнение. Думаю, комментарии излишни. Получаемые от грамотной реализации Externalizable преимущества в скорости с лихвой компенсируют затраты на эту самую реализацию. Грамотной – в смысле, целиком и completamente реализованной самостоятельно, без использования имеющихся механизмов сериализации целых un objetoов (в основном это методы writeObject/readObject). Использование же имеющихся механизмов и/o смешивание со стандартной сериализацией способно свести скоростные преимущества Externalizable на нет. Однако есть и ...

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

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

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

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

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

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