Мои Конспекты
Главная | Обратная связь


Автомобили
Астрономия
Биология
География
Дом и сад
Другие языки
Другое
Информатика
История
Культура
Литература
Логика
Математика
Медицина
Металлургия
Механика
Образование
Охрана труда
Педагогика
Политика
Право
Психология
Религия
Риторика
Социология
Спорт
Строительство
Технология
Туризм
Физика
Философия
Финансы
Химия
Черчение
Экология
Экономика
Электроника

Рассмотрите возможность использования специализированной сериализованной формы



 

Если вы создаете класс в условиях дефицита времени, то, как правило, имеет смысл сконцентрировать усилия на построении самого лучшего АРI. Иногда это означает создание "одноразовой" реализации, которая в следующей версии поменяет­ся. Обычно это проблем не вызывает, однако если данный класс реализует интерфейс Serializable и использует при этом сериализованную форму, предоставленную по умолчанию, вам уже никогда не удастся полностью избавиться от этой временной реализации, и она всегда будет навязывать вам именно эту сериализованную форму. Это не теоретическая проблема. Такое уже происходило с несколькими классами из библиотек для платформы Java, такими как B1gInteger.

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

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

 

 

 

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

// Хороший кандидат для использования формы,

// предлагаемой по умолчанию

public class Name implements Serializable

/**

* Последнее имя (англ. ). Не должно быть пустым (non-null)

* @serial

*/

private String lastName;

/**

* Первое имя. Не должно быть пустым.

*'@serial

*/

private String firstName;

/**

* Средний инициал или '\uOOOO', если инициал отсутствует

* @serial

*/

private char middlelnitial;

// Остальное опущено

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

Если вы решите принять сериализованную форму, предлагаемую по умол­чанию, во многих случаях сохранение инвариантов и безопасность требуют реа­лизации метода readObj ect. В случае с классом Name метод readObject мог бы гарантировать, что поля lastName и firstName не будут иметь значения null. Эта тема подробно рассматривается в статье 56.

Заметим, что поля lastName, firstName и middlelnitial сопровождает коммента­рий к документации, хотя все они являются закры~ыми. Это необходимо поскольку эти закрытые поля определяют открытый АР!: сериализованную форму класса, а всякий открытый АР! должен быть документирован. Наличие тега @serial говорит утилите Javadoc о том, что эту информацию необходимо поместить на специальную страницу, где описываются сериализованные формы.

 

 

 

Теперь рассмотрим класс, который представляет набор строк (забудем на минуту о том, что для этого лучше было бы взять в библиотеке одну из стандартных реализа­ций интерфейса List):

 

// Ужасный кандидат на использование сериализованной формы,

// предлагаемой по умолчанию

public class StringList implements Serializable {

private int size = 0;

private Entry head = null;

private static class Entry implements Serializable {

String data;

Entry next;

Entry previous; }

// Остальное опущено

}

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

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

 

· Она навсегда связывает внешний API класса с его текущим внутренним представлением. В приведенном примере закрытый

класс StringList.Entry становится частью открытого API. Даже если в будущей версии внутреннее представление StringList поменяется,

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

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

слишком велика, ее запись на диск или передача по сети

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

 

 

 

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

· Она может вызвать переполнение стека. Процедура сериализации, реализуемая по умолчанию, выполняет рекурсивный обход графа объектов, что может вызвать переполнение стека даже при обработке графов среднего размера. На моей машине к переполнению стека приводит сериализация экземпляра StringList с 1200 элементами. Количество элементов, вызывающее эту проблему, меняется в зависимости от реализации ]УМ. В некоторых реализациях этой проблемы вообще не существует.

Правильная сериализованная форма для класса StringList - это количество строк в списке, за которым следуют сами строки. Это соответствует освобожденным от деталей физической реализации логическим данным, представляемым классом StringList. Приведем исправленный вариант StringList, содержащий методы writeObject и readObject, которые реализуют правильную сериализованную форму. Напомним, что модификатор transient указывает на то, что экземпляр поля должен быть исключен из сериализованной формы, применяемой по умолчанию:

// Класс StringList справильной сериализованной формой

public class StringList implements Serializable

private transient int size = о;

private transient Entry head = null;

// Больwе нет реализации Serializablel

private static class Entry {

String data;

Entry next;

Entry previous;

}

// Добавляет указанную строку в конец списка

public void add(String s) { ... }

/**

* Сериализует данный экземпляр <tt>StringList</tt>.

*

* @serialData Показывается размер списка (количество

* содержащихся в нем строк) «tt>int</tt», за которым * в правильной последовательности

* следуют все

* элементы списка (каждый в виде <tt>String</tt». */

 

 

 

private void writeObject(ObjectOutputStream s)

throws IОЕхсерtion {

s.defaultWriteObject();

s.writelnt(size);

// Выписываем все элементы в правильном порядке

for (Entry е = head; е != null; е = e.next)

s.writeObject(e.data);

}

private void readObject(ObjectlnputStream s)

throws IOException, ClassNotFoundException {

s.defaultReadObject();

int size = s. readlnt();

// Считываем все элементы и вставляем

for (int i = 0; i < size; i++)

add((String)s. readObject()); }

// Остальное опускаем

}

 

Заметим, что из метода writeObject вызывается defaultWriteObject, а из метода readObject делается вызов defaultReadObject, несмотря на то, что ни одно из полей класса StringList не попадает в сериализованную форму. Если все экземпляры полей имеют модификатор transient, то формально можно обойтись без вызова методов defaultWriteObject и defaultReadObject, но это не рекомендуется. Даже если все экземпляры полей имеют модификатор trаnsiеnt, вызов defaultWriteObject оказывает влияние на сериализованную форму, в результате чего значительно повы­шается гибкость сериализации. Полученная форма оставляет возможность в последу­ющих версиях добавлять в форму новые экземпляры полей, сохраняя при этом прямую и обратную совместимость с предыдущими версиями. Так, если сериализовать экземп­ляр класса в более поздней версии, а десериализовать в более ранней версии, появив­шиеся поля будут проигнорированы. Если бы более ранняя версия метода readObject не вызывала метод defaultReadObject, десериализация закончилась бы инициированием StreamCorruptedException.

Заметим также, что хотя метод writeObject является закрытым, он сопровождается комментариями к документации. Объяснение здесь то же, что и в случае с комментариями для закрытых полей в классе Name. Этот закрытый метод определяет сериализованную форму открытый API, а открытый API должен быть описан в документации. Как и тег @seria1 в случае с полями, тег @serialData для методов говорит утилите Javadoc о том, что данную информацию необходимо поместить на страницу с описанием сериализованных форм.

 

 

 

 

Что касается производительности, то при средней длине строки, равной десяти символам, сериализованная форма для исправленной версии StгiпgList будет за­нимать вдвое меньше места, чем в первоначальном варианте. На моей машине се­риализация исправленного варианта StгiпgList при длине строк в десять символов выполняется примерно в два с половиной раза быстрее, чем сериализация первона­чального варианта. 11 наконец, у исправленного варианта не возникает проблем с переполнением стека, а потому практически нет верхнего ограничения на размер StringList, для которого можно выполнить сериализацию.

Сериализованная форма, предлагаемая по умолчанию, плохо подходит для класса StringList, но есть классы, для которых она подходит еще меньше. Для StгiпgList сериализованная форма, применяемая по умолчанию, не имеет гибкости и работает медленно. Однако она является правильнои в том смысле, что в результате сериали­зации и десериализации экземпляра StringList получается точная копия исходного объекта, и все его инварианты будут сохранены. Но для любого объекта, чьи инвари­анты при вязаны к деталям реализации, это не так.

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

11спользуете вы или нет сериализованную форму, предлагаемую по умолчанию, каждый экземпляр поля, не помеченный модификатором trаnsiеnt, будет сериализо­ван при вызове метода defaultWriteObject. Поэтому каждое поле, которое можно не заносить в форму, нужно пометить этим модификатором. К таковым относятся из­быточные поля, чьи значения можно вычислить по таким "первичным полям данных", как кэшированное значение хэша. Сюда также относятся поля, чьи значения меняются при повторном запуске JVM. Например, это может быть поле типа 10ng, в котором хранится указатель на местную' (native) структуру данных. Прежде чем согласиться на запись какого-либо поля в сериализованной форме, убедитесь в том, что его значение является частью логического состояния данного объекта. Если вы поль­зуетесь специальной сериализованной формой, большинство или даже все экземп­ляры полей нужно пометить модификатором trаnsiеnt, как в примере с классом StringList.

Если вы пользуетесь сериализованной формой, предлагаемой по умолчанию, и к тому же пометили одно или несколько полей как trаnsiеnt, помните о том, что при десериализации экземпляра эти поля получат значения по умолчанию: пu11 для полей ссылок на объекты, нуль для простых числовых полей и false для полей типа

 

 

 

 

boolean [JLS, 4.5.5]. Если для какого-либо из этих полей указанные значения не­приемлемы, необходимо предоставить метод readObject, который вызывает метод defaultReadObject, а затем восстанавливает приемлемые значения в полях, помечен­ных как transient (статья 56). Альтернативный подход заключается в том, чтобы отложить инициализацию этих полей до первого вызова.

Независимо от того, какую сериализованную форму вы выберете, в каждом сериализуемом классе, который вы пишите, явным образом декларируйте serial version UID. Тем самым вы исключите этот идентификатор из числа возможных причин несовместимости (статья 54). Это также даст некоторый выигрыш в произво­дительности. Если seriaL version UID не представлен, то при выполнении программы для его генерации потребуется выполнить трудоемкие вычисления. Для декларации seriaд version UID добавьте в ваш класс строку:

private static final long serialVersionUID = raтdoтLoтgValue;

// Произвольное число типа long

 

Не важно, какое значение вы выберете для randoтLongValиe. Общепринятая прак­тика предписывает генерировать это число, запуская для класса утилиту serialver. Однако можно взять число просто из "воздуха". Если вам когда-нибудь захочется создать новую версию класса, которая несовместима с имеющимися версиями, доста­точно будет поменять значение в этой декларации. В результате попытки десериали­зировать экземпляры, сериализованные в предыдущих версиях, будут заканчиваться инициированием исключения InvalidClassException.

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