Главная » Java, Советы » Соблюдайте осторожность при реализации интерфейса SerializabIe

0

Чтобы сделать экземпляры класса сериализуемыми, достаточно добавить в его декларацию слова "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. Если класс предназначен для наследования и не является сериализуемы, может оказаться, что для него невозможно написать сериализуемый подкласс. В частности, этого нельзя сделать, если у суперкласса нет доступного конструктора без параметров. Следовательно, для несериализуемого класса, который предназначен для наследования, Вы должны рассмотреть возможность создания конструктора без параметров. Часто это не требует особых усилий, поскольку многие классы, предназначенные для наследования, не имеют состояния. Но так бывает не всегда. 201

Самое лучшее _ это создавать объекты, у которых все инварианты уже установлены (статья 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, хотя и не является обязательным условием.

Источник: Джошуа Блох, Java TM Эффективное программирование, Издательство «Лори»

По теме:

  • Комментарии