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


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

Соблюдайте осторожность при реализации интерфейса SerializabIe



 

 

Чтобы сделать экземпляры класса сериализуемыми, достаточно добавить в его декларацию слова "implements Serializable". Поскольку это так легко, широкое распространение получило неправильное представление, что сериализация требует от программиста совсем небольших усилий. На самом деле все гораздо сложнее.

Значительная доля затрат на реализацию интерфейса Serializable связана с тем, что уменьшается возможность изменения реализации класса в последую­щих версиях. Когда класс реализует интерфейс Serializable, соответствующий ему поток байтов (сериализованная форма - serialized Form) становится частью его внеш­него API. И как только ваш класс получит широкое распространение, вам придется поддерживать соответствующую сериализованную форму точно так же, как вы обяза­ны поддерживать все остальные части интерфейса, предоставляемого клиентам. Если вы не приложите усилий к построению специальной сериализованноu формы (custom

 

 

 

serialized form) , а примете форму, предлагаемую по умолчанию, эта форма окажется навсегда связанной с первоначальным внутренним представлением класса. Иначе гово­ря, если вы принимаете сериализованную форму, которая предлагается по умолчанию, те экземпляры полей, которые были закрыты или доступны только в пакете, станут частью его внешнего API, и практика минимальной доступности полей (статья 12) потеряет свою эффективность как средство скрытия информации.

Если вы принимаете сериализованную форму, предлагаемую по умолчанию, а за­тем меняете внутреннее представление класса, это может привести к таким изменени­ям в форме, что она станет несовместима с предыдущими версиями. Клиенты, которые пытаются сериализовать объект с помощью старой версии класса и десериализовать его уже с помощью новой версии, получат сбой программы. Можно поменять внутрен­нее представление класса, оставив первоначальную сериализованную форму (с по­мощью методов ObjectOutputSt геат. putFields и ObjectOutputSt геат, readF1elds), но этот механизм довольно сложен и оставляет в исходном коде программы видимые изъяны. Поэтому тщательно выстраивайте качественную сериализованную форму, с которой вы сможете отправиться в долгий путь (статья 55). Эта работа усложняет создание приложения, но дело того стоит. Даже хорошо спроектированная сериализо­ванная форма ограничивает дальнейшее развитие класса, плохо же спроектированная форма может его искалечить.

Простым примером того, какие ограничения на изменение класса накладывает сериализация, могут служить уникальные идентификаторы потока (stream иniqиe identifier), более известные как seria/ versio'n и 1 D. С каждым сериализуемым классом связан уникальный идентификационный номер. Если вы не указываете этот идентификатор явно, декларируя поле private static final long с названием serialVersionUID, система генерирует его автоматически, используя для класса сложную схему расчетов. При этом на автоматически генерируемое значение оказывают влияние название клас­са, названия реализуемых им интерфейсов, а также все открытые и защищенные члены. Если вы каким-то образом поменяете что-либо в этом наборе, например, доба­вите простой и удобный метод, изменится и автоматически генерируемый serial version UID. Следовательно, если вы не будете явным образом декларировать этот идентифи­катор, совместимость с предыдущими версиями будет потеряна.

Второе неудобство от реализации интерфейса Serializable заключается в том, что повышается вероятность появления ошибок и дыр в защите. Объекты обычно создаются с помощью конструкторов, сериализация же представляет собой механизм создания объектов, который выходит за рамки языка /ava. Принимаете ли вы схему, которая предлагается по умолчанию, или переопределяете ее, десериализация - это "скрытый конструктор", имеющий все те же проблемы, что и остальные конструкторы. Поскольку явного конструктора здесь нет, легко упустить из виду то, что при десериализации вы должны гарантировать сохранение всех инвариантов, устанавливаемых настоящими конструкторами, и исключить возможность получения злоумышленником доступа к внутреннему содержимому создаваемого объекта. Понадеявшись на меха­низм десериализации, предоставляемый по умолчанию, вы можете получить объекты, которые не препятствуют несанкционированному доступу к внутренним частям и раз­рушению инвариантов (статья 56).

 

 

 

Третье неудобство реаливации интерфейса Serializable связано с тем, что выпуск новой версии класса сопряжен с большой работой по тестированию. При пересмотре сериализуемого класса важно проверить возможность сериализации объекта в новой версии и последующей его десериализации в старой и наоборот. Таким образом, объем необходимого тестирования прямо пропорционален произведе­нию числа сериализуемых классов и числа имеющихся версий, что может быть боль­шой величиной., К подготовке таких тестов нельзя подходить формально, поскольку, помимо совместимости на бинарном уровне, вы должны проверять совместимость на уровне семантики. Иными словами, необходимо гарантировать .. не только успеш­ность процесса сериализации-десериализации, но и то, что он будет создавать точную копию первоначального объекта. И чем больше изменяется сериализуемый класс, тем сильнее потребность в тестировании. Если при написании класса специальная сериали­зованная форма была спроектирована тщательно (статья 55), потребность в проверке уменьшается, но полностью не исчезает.

Реализация интерфейса Serializable должна быть хорошо продумана. У это­го интерфейса есть реальные преимущества: его реализация играет важную роль, если класс должен участвовать в какой-либо схеме, которая для передачи или обеспечения живучести объекта использует сериализацию. Более того, это упрощает применение класса как составной части другого класса, который должен реализовать интерфейс Serializable. Однако с реализацией интерфейса Serializable связано и множество неудобств. Реализуя класс, соотносите неудобства с преимуществами. Практическое правило таково: классы значений, такие как Date и BigInteger, и большинство классов коллекций обязаны реализовывать этот интерфейс. Классы, представляющие актив­ные сущности, например пул потоков, должны реализовывать интерфейс Serializable крайне редко. Так, в версии 1.4 появился механизм сохранения компонентов JаvаВеап, который использует стандарт XML, а потому этим компонентам больше не нужно реализовывать интерфейс Serializable.

Классы, предназначенные для наследования (статья 15), редко должны реализовывать Ser1alizable, а интерфейсы - редко его расширять. Нарушение этого правила связано с большими затратами для любого, кто пытается расширить такой класс или реализовать интерфейс. В ряде случаев это правило можно нарушать. Например, если класс или интерфейс создан в первую очередь для использования

в некоторой системе, требующей, чтобы все ее участники реализовывали интерфейс Ser1al1zable, то лучше всего, чтобы этот класс (интерфейс) реализовывал (расши­рял) Ser1a11zable.

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

 

 

 

Самое лучшее _ это создавать объекты, у которых все инварианты уже уста­новлены (статья 13). Если для установки инвариантов необходима информация от клиента, это будет препятствовать использованию конструктора без параметров. Бесхитростное добавление конструктора без параметров и метода инициализации в класс, остальные конструкторы которого устанавливают инварианты, усложняет пространство состояний этого класса и увеличивает вероятность появления ошибки.

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

один конструктор:

 

public AbstractFoo(int х, int у) { ... }

 

Следующее преобразование добавляет защищенный конструктор без параметров и отдельный метод инициализации. Причем метод инициализации имеет те же пара­метры и устанавливает те же инварианты, что и обычный конструктор:

 

public abstract class AbstractFoo {

private int x, y; // Состояние

private boolean initialized = false;

 

public AbstractFoo(int x, int y) { initialize(x, y); }

 

/**

* Данный конструктор и следующий за ним метод позволяют методу

* readObject в подклассе инициализировать наше внутреннее состояние.

*/

protected AbstractFoo() { }

 

protected final void initialize(int x, int y) {

if (initialized)

throw new IllegalStateException(

"Already initialized");

this.x = x;

this.y = y;

// ... // Делает все остальное, что делал прежний конструктор

initialized = true;

}

 

/**

*Эти методы предоставляют доступ к внутреннему состоянию класса,

* и потому его можно сериализовать вручную, используя

* метод writeObject из подкласса

*/

 

protected final int getX() { return x; }

protected final int getY() { return y; }

 

// Должен вызываться для всех открытых методов экземпляра

private void checkInit() throws IllegalStateException {

if (!initialized)

throw new IllegalStateException("Uninitialized");

}

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

}

 

Все методы в экземпляре AbstractFoo, прежде чем выполнять свою работу, должны вызывать checklnit. Тем самым гарантируется быстрое и четкое аварийное завершение этих методов в случае, если неудачно написанный подкласс не инициали­зировал соответствующий экземпляр. Имея этот механизм взамен прежнего, можно перейти к реализации сериалнзуемого подкласса:

 

import java.io.*;

 

public class Foo extends AbstractFoo implements Serializable {

private void readObject(ObjectInputStream s)

throws IOException, ClassNotFoundException {

s.defaultReadObject();

 

// Ручная десериализация и инициализация состояния суперкласса

int x = s.readInt();

int y = s.readInt();

initialize(x, y);

}

 

private void writeObject(ObjectOutputStream s)

throws IOException {

s.defaultWriteObject();

 

// Ручная сериализация состояния суперкласса

s.writeInt(getX());

s.writeInt(getY());

}

 

// Конструктор не использует никаких причудливых механизмов

public Foo(int x, int y) { super(x, y); }

}

 

 

Внутренние классы (статья 18) редко должны (если вообще должны) реа­лизовывать интерфейс Serializable. Для размещения ссылок на экземпляры контейнера (enclosing instance) и значений локальных переменных из окружения они

 

 

используют искусственные поля (synthetic field), генерируемые компилятором. Как именно эти поля соотносятся с декларацией класса, не конкретизируется. Не конкре­тизируются также названия анонимных и локальных классов. Поэтому выбор для внутреннего класса серилизованной формы, предлагаемой по умолчанию,­ плохое дизайнерское решение. Однако статический класс-член вполне может реализовывать интерфейс Serializable.

Подведем итоги. Легкость реализации интерфейса Sеrializable обманчива. Реа­лизация интерфейса Serializable - серьезное обязательство, которое следует брать на себя с осторожностью, если только не предполагается выбросить класс после не­долгого использования. Особое внимание требует класс, предназначенный для на­следования. Для таких классов границей между реализацией интерфейса Sеrializable в подклассах и его запретом является создание доступного конструктора без парамет­ров. Это позволяет реализовать в подклассе интерфейс Sеrializable, хотя и не явля­ется обязательным условием.