JavaRush /Java Blog /Random-KO /직렬화는 그대로입니다. 2 부
articles
레벨 15

직렬화는 그대로입니다. 2 부

Random-KO 그룹에 게시되었습니다

성능

이미 말했듯이 표준 직렬화는 Reflection API를 통해 작동합니다. 즉, 직렬화의 경우 직렬화되는 개체의 클래스를 가져오고, 여기에서 필드 목록을 가져오고, 루프의 모든 필드에 대해 다양한 조건을 확인합니다( 일시적 인지 아닌지, 개체인 경우 외부화 가능 또는 직렬화 가능 ). 값은 스트림에 기록되고 리플렉션을 통해 필드에서도 검색됩니다 . 일반적으로 상황은 분명합니다. 이 방법과 달리 확장 직렬화를 사용하는 경우 전체 절차는 개발자가 직접 제어합니다. 이것이 속도 측면에서 어떤 이점을 주는지는 아직 알 수 없습니다. 그래서 테스트 조건입니다. 임의 구조의 객체입니다. 두 가지 옵션 - 하나는 직렬화 가능 , 두 번째는 외부화 가능 . 두 옵션 모두 특정 수의 개체가 임의의 데이터(각 개체 쌍에 대해 동일)로 초기화된 다음 컨테이너에 배치됩니다. 컨테이너는 어떤 경우에는 직렬화 가능 하고 다른 경우에는 외부화 가능합니다 . 다음으로 컨테이너는 시간 측정을 통해 직렬화 및 역직렬화됩니다. ant용 빌드 파일 과 함께 전체 테스트 코드는 여기(serialization.zip)에서 찾을 수 있습니다(소스 사이트에서 다운로드할 수 있음). 본문에서는 발췌 내용만 제공하겠습니다. 직렬화 가능 객체에는 다음 필드 세트가 포함되어 있습니다. private int fieldInt; private boolean fieldBoolean; private long fieldLong; private float fieldFloat; private double fieldDouble; private String fieldString; 테스트에는 외부화 가능 컨테이너 의 세 가지 구현이 포함되어 있습니다. 첫 번째인 ContainerExt1 은 가장 간단합니다. 이는 포함된 java.util.List 객체 의 직렬화입니다 . 두 번째 구현인 ContainerExt2는 모든 기존 객체를 순차적으로 직렬화하고 객체 앞에 객체 수를 추가합니다.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-методы an objectов: 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-файле задано количество создаваемых an objectов – 100000. Другое количество может быть задано с помощью параметра командной строки -Dobjcount= . Итак, Howовы результаты выполнения теста? На 100000 создаваемых an objectов (результаты могут незначительно отличаться от запуска к запуску): 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 What мы видим? Первый способ реализации Externalizable даже несколько хуже стандартной сериализации. Сериализация занимает немного больше времени, десериализация сравнима. Размеры файлов тоже немного в пользу стандартной сериализации. Вывод – простейшая сериализация контейнера преимуществ не дает: +15% при сериализации, десериализация отличается на доли процента, причем How в одну, так и в другую сторону. Второй способ реализации Externalizable по характеристикам практически идентичен первому. Чуть быстрее сериализация, но все равно проигрывает стандартной, десериализация чуть выигрывает. Размер file практически идентичен первому способу (разница – 38 byte). Выигрыша по сравнению со стандартной сериализацией нет – +10% при сериализации, -8% при десериализации. Третий способ реализации Externalizable. Вот тут есть на что посмотреть! Сериализация быстрее в 15 раз! Естественно, плюс-минус, но тем не менее – разница на порядок! Десериализация быстрее практически в 11 раз! Difference тоже на порядок! Опять же плюс-минус, но мне не удавалось получить разницу меньше, нежели в 5 раз. Ну и разница в размере file -13%. Как маленькое, но приятное дополнение. Думаю, комментарии излишни. Получаемые от грамотной реализации Externalizable преимущества в скорости с лихвой компенсируют затраты на эту самую реализацию. Грамотной – в смысле, целиком и fully реализованной самостоятельно, без использования имеющихся механизмов сериализации целых an objectов (в основном это методы writeObject/readObject). Использование же имеющихся механизмов и/or смешивание со стандартной сериализацией способно свести скоростные преимущества Externalizable на нет. Однако есть и ...

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

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

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

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

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

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