[an error occurred while processing this directive] IT • archiv :: Print

IT • archiv


[an error occurred while processing this directive]

[an error occurred while processing this directive]

Эффективное программирование на Java

[an error occurred while processing this directive](none) [an error occurred while processing this directive](none)[an error occurred while processing this directive] ::
[an error occurred while processing this directive](none)
[an error occurred while processing this directive]([an error occurred while processing this directive]Джошуа Блоч[an error occurred while processing this directive])

[an error occurred while processing this directive](none)

Эта статья является частью предварительной версии книги Joshua Bloch'а "Эффективное программирование на Джава" ("Effective Java Programming"). Эта книга выходит в серии "Джава из первых рук" ("Java from the Source") которая будет опубликована в издательстве Addison-Wesley в конце 2000 - начале 2001 года. Здесь представлена вторая глава книги - "Замены для отсутствующих в Джава конструкций языка Си".

От автора.

В этой главе обсуждаются конструкции Си, которые отсутствуют в Джава и предлагаются наиболее приемлемые альтернативы для их замены. Наиболее интересна третья часть статьи, в которой рассказывается о типизированных перечислениях (typesafe enums), в качестве замены перечислений имеющихся в Си. Этот проектировочный шаблон не слишком известен и имеет несколько преимуществ перед альтернативами используемыми сейчас.

Книга, которая предварительно названа "Эффективное программирование на Джава" , это попытка дать Джава то, что для Си++ дала книга Scott Meyers'а "Эффективный Си++" ("Effective C++", Addison-Wesley, 1992). Она концентрирует в пятидесяти или меньшем количестве эссе (длинной каждое примерно в четыре страницы) вещи которые опытные программисты на Джава стараются делать или наоборот не делать. Эссе имеют названия наподобие "Избегайте финализаторов" или "Предпочитайте делегирование, а не наследование". Книга объясняет почему практикование тех или иных решений желательно или наоборот не желательно, а также рассматривает альтернативные практики, которые представляются менее предпочтительными. Все эссе самодостаточны и не сильно связанны друг с другом внутри каждой из глав книги. Каждое эссе содержит один или несколько примеров.

Книга по большей части (но не слепо) ограничена "ядром" платформы Джава: самим языком программирования и пакетами java.lang и java.util. Как следствие почти все эссе должны быть интересны всем программирующим на Джава.

Целевая аудитория книги - это средне опытные программисты на Джава, которые уже имеют опыт работы с языком и ключевыми библиотеками. Несмотря на это, опытные программисты также найдут в некоторых эссе пищу для размышлений, особенно программисты с укоренившемеся навыками, полученными во время многих лет программирование на других языках программирования, таких как Си и Си++. Книга не о Шаблонах Проектирования (Design Patterns) и не подразумевает у читающего какие либо знания в этой области, однако, имеет подобный же подход.

Об авторе.

Joshua Bloch - ведущий инженер Sun Microsystems, где он занимается архитектурой в Группе Ядра Платформы Джава. Он спроектировал и реализовал библиотеку структур данных (Collections Framework) и пакет java.math, а также принимал участие в разработке других частей платформы Джава. Сейчас он руководит работами по добавлению в платформу Джава утверждений (assertions) и предпочтений (preferences). До этого он был главным системным проектировщиком в Корпорации Transarc, где спроектировал и реализовал многие части распределенной системы обработки транзакций Encinia. Он является доктором философии от Университета Кэрнеги-Меллоуна и B.S. от Колумбийского Университета.

Joshua Bloch. Замены для отсутствующих в Джава конструкций языка Си.

Язык программирования Джава имеет много общего с языком Си, но некоторые конструкции Си отсутствуют в Джава. В большинстве случаев очевидно, почему та или иная конструкция исключена из языка и как можно обходится без нее. В этой же главе обсуждается замены для тех конструкций Си, альтернативы которым не столь очевидны.

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

Замечание: даже если вы решили пропустить эту главу, все же вам, скорее всего, будет полезно прочитать раздел "Замена перечислений классами", в которой обсуждается шаблон типизированных перечислений, как альтернатива перечислениям Си. Этот шаблон проектирования не так широко известен на данный момент и имеет несколько преимуществ перед широко используемыми сейчас методами.

Замена структур (structs) и псевдонимов типов (typedefs) классами.

Структуры и псевдонимы типов были исключены из Джава по причине того, что с помощью классов можно сделать все то же самое и даже большее. Структуры, по большой части - это группы полей данных объединенные в одном объекте; класс ассоциирует с этим объектом операции и позволяет скрывать данные от пользователя объекта. Другими словами, класс может инкапсулировать данные в объекте, который доступен исключительно через свои методы, обеспечивая тем самым реализатору класса свободу изменения внутреннего устройства по ходу развития системы.

После первого знакомства с Джава некоторые программисты на Си считают, что классы слишком тяжеловесны для замены структур, но на самом деле это не так. Вырожденные классы, состоящие только из полей данных, почти эквивалентны структурам во всех отношениях:

// Вырожденные классы, наподобие этого,
// никогда не должны быть объявлены как
// публичные (public)
class Point {   // Точка
    public float x;
    public float y;
}

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

class Point {
    private float x;
    private float y;
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }
    public float getX() { return x; }
    public float getY() { return y; }
    public void setX(float x) { this.x = x; }
    public void setY(float y) { this.y = y; }
}

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

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

Замена объединений (unions) иерархиями классов.

Конструкция объединения наиболее часто используется для определения структур которые могут содержать данные более чем одного типа. Такие структуры состоят, по крайней мере, из двух полей: объединения и тэга (tag). Тэг - простое поле которое используется для указания значение какого типа, из множества могущих содержаться в этом объединении, содержится в нем сейчас. Тэг обычно является перечислением созданным специально для этой цели. Структуру содержащую объединение и тэг иногда еще называют дискриминантным объединением (discriminated union, ДО).

В следующем примере, написанном на Си, тип shape_t - дискриминантное объединение, которое может быть использовано для представления как прямоугольника так и окружности. Функция area принимает в качестве аргумента указатель на структуру shape_t и возвращает ее площадь или -1.0 если структура имеет неверное значение:

typedef enum {RECTANGLE, CIRCLE} shapeType_t;   // тип Фигуры
typedef struct {
    double length;              // длина
    double width;               // ширина
} rectangleDimensions_t;        // размерыПрямоугольника
typedef struct {
    double radius;              // радиус
} circleDimensions_t;           // размерыОкружности
typedef struct {
        shapeType_t tag;        // тэг
    union {
        rectangleDimensions_t rectangle;        // прямоугольник
        circleDimensions_t    circle;           // окружность
    } dimensions;               // размеры
} shape_t;      // фигура
double area(shape_t *shape) {   // площадь
        switch(shape->tag) {
        case RECTANGLE: {       // ПРЯМОУГОЛЬНИК
                double length = shape->dimensions.rectangle.length;
                double width  = shape->dimensions.rectangle.width;
                return length * width;
                }
        case CIRCLE: {          // ОКРУЖНОСТЬ
                double r = shape->dimensions.circle.radius;
                return M_PI * (r*r);
                }
                default:
                return -1.0; /* Неправильный тэг */
        }
}

Проектировщики Джава решили исключить объединения потому, что есть намного более лучший механизм определения типа данных способного представлять объекты разных типов - подтипы (subtyping). Дискриминантные объединения на самом деле всего лишь бледная имитация иерархии классов.

Для преобразования дескриминантного объединения в иерархию классов, определим абстрактный класс, содержащий абстрактные методы для каждой операции, поведение которой зависит от значения тэга. В вышеприведенном примере есть только одна такая операция - area. Этот класс будет корнем иерархии классов. Если какие-либо операции не зависят от значения тэга, тогда сделайте эти операции конкретными методами корневого класса. Аналогично, если в дескриминантном объединении есть поля данных кроме тэга и объединения, тогда эти поля представляют общие данные для всех типов и поэтому должны быть добавлены в корневой класс. В нашем примере таких операций или полей данных нет.

Затем определим конкретные подклассы корневого абстрактного класса для каждого типа, который может представлять ДО. В вышеприведенном примере такими типами являются окружность и прямоугольник. Включим в каждый подкласс поля данных соответствующие его типу. Радиус в окружность, и длину и ширину в прямоугольник. Также включим в каждый подкласс соответствующие реализации каждого абстрактного метода корневого класса. Мы получили иерархию классов соответствующую дескриминантному объединению из вышеприведенного примера:

abstract class Shape {  // Фигура
    abstract double area();             // площадь
}
class Circle extends Shape {    // Окружность
    final double radius;                // радиус
    Circle(double radius) { this.radius = radius; }
    double area() { return Math.PI * radius*radius; }
}
class Rectangle extends Shape { // Прямоугольник
    final double length;                // длина
    final double width;                 // ширина
    Rectangle(double length, double width) {
        this.length = length;
        this.width  = width;
    }
    double area() { return length * width; }
}

Иерархия классов имеет несколько преимуществ перед дескриминантным объединением, главное из которых то, что она обеспечивает типизацию. В примере выше каждый объект Shape является либо экземпляром Circle, либо Rectangle. Очень легко создать структуру shape_t, которая является просто "мусором", так как зависимость между значением тэга и значениями других полей объединения не поддерживается языком. Если тэг указывает на то, что shape_t представляет прямоугольник, а на самом деле объединение содержит данные для окружности, то это может привести к плачевным последствиям. Даже если структура правильно проинициализирована, все еще возможно передать ее в функцию, которая не понимает значения тэга.

Второе преимущество иерархии классов, что код становится проще и яснее. Код с ДО полон шаблонного кода: объявление перечислимого типа, объявление поля тэга, переключение по полю тэга, обработка неожидаемых значений тэга и тому подобное. Код с ДО становится еще менее читаемым вследствие того, что операции для различных типов перемешаны друг с другом, вместо того, что бы быть разделенными по типу.

Третье преимущество - легкая расширяемость, даже когда несколько разработчиков работают независимо друг от друга. Для расширения иерархии просто добавьте подкласс. Если вы забудете переопределить один из абстрактных методов, компилятор ясно укажет вам на это. Для расширения дескриминантного объединения вы должны добавить новое значение в перечисление, а также новую ветку в операторы переключения (switch statements) для каждой операции. Если же вы забудете добавить ветку в каком-нибудь методе, то вы обнаружите это только во время выполнения и только в случае если вы проверяете тэг на предмет неопознанных значений и генерируете соответствующее сообщение.

Четвертое преимущество иерархии классов перед ДО - это то, что она может отражать естественные иерархические отношения между типами, тем самым увеличивая гибкость и улучшая проверку типов на этапе компиляции. Допустим, что ДО в нашем примере также позволяет представлять и квадраты. Тогда может быть построена соответствующая иерархия классов отображающая тот факта, что квадрат это особый случай прямоугольника:

class Square extends Rectangle {        // Квадрат
    Square(double side) { super(side, side); }
    double side() {             // сторона
        return length; // или, что эквивалентно, width
    }
}

Вышеприведенная иерархия классов, не единственная которой можно было бы заменить ДО. Построенная иерархия подразумевает некоторые проектировочные решения, которые стоит обсудить. Классы в иерархии, за исключением Square, доступны через их поля, а не через методы. Это было сделано для краткости текста примера, но такое решение не может считаться приемлемым, если классы публичные. Объекты созданных классов не изменяемы, что не всегда то, что нужно, но в общем случае это хорошая вещь.

Так как в Джава нет конструкции объединения, то вы можете решить, что опасность написания ДО исчезла. Однако все еще возможно написание кода с теми же недостатками, которые имеет ДО. Каждый раз, когда вы пытаетесь написать класс с чем-то напоминающем поле тэга, подумайте об устранении тэга и замене класса иерархией классов.

Другое использование конструкции объединения в Си/Си++ (не имеющее ни какого отношения к дескриминантному объединению) - это обращение к внутреннему представлению части данных, то есть преднамеренное нарушение типизации. Такое использование демонстрирует следующий фрагмент кода на Си, который печатает машинно-зависимое шестнадцатеричное представление значения типа float:

   union {
        float f;
        int   bits;
    } sleaze;
    /* Сохранение значение в одном из полей объединения... */
    sleaze.f = 6.699e-41;
    /* ...и чтение из другого. */
    printf("%x\n", sleaze.bits);

Это в высшей степени не переносимое использование не имеет аналога в Джава. На самом деле, оно противоречит самому духу этого языка, который гарантирует безопасность типов и прилагает большие усилия для изоляции программиста от машинно-зависимых внутренних представлений данных. Пакет java.lang содержит методы для перевода чисел с плавающей точкой в битовое представление, однако эти методы переводят значения в строго специфицированное битовое представление, что гарантирует переносимость. Фрагмент кода на Джава приведенный ниже является примерным эквивалентом переведенного выше кода на Си/Си++, но гарантирует печать одного и того же результата не зависимо от платформы на которой он запущен:

   System.out.println(Integer.toHexString(
                   Float.floatToIntBits(6.699e-41F)));

Замена перечислений классами.

Конструкция перечислений языка Си была исключена из Джава. Номинально, эта конструкция определяет перечислимый тип - тип разрешенные значения, которого представляет собой фиксированный набор констант. К сожалению перечисления не очень хорошо справляются с работой по определению перечислимых типов. Они всего лишь определяют набор поименованных целочисленных констант, не обеспечивая никакой безопасности типа и лишь немного улучшая удобство использования. Си разрешает не только такой код:

   typedef enum {FUJI, PIPPIN, GRANNY_SMITH} apple_t;    // яблоки
    typedef enum {NAVEL, TEMPLE, BLOOD} orange_t;    // апельсины
    /* Смешивание яблок и апельсинов */
    orange_t myFavorite = PIPPIN;
но и такое зверство:
    /* Попробуйте апельсиново-яблочный соус!!! */
    orange_t x = (FUJI - PIPPIN)/TEMPLE;

Конструкция перечислений не обеспечивает для объявляемых констант изолированного пространства имен, что ведет к конфликту объявлений с объявлением orange_t приведенным выше:

   typedef enum {BLOOD, SWEAT, TEARS} fluid_t; // флюиды

Кроме того, типы, определяемые с помощью перечислений хрупки. Добавление констант к типу без перекомпиляции его клиентов ведет к непредсказуемым последствиям, если только не были предприняты меры к сохранению значений всех уже определенных до этого констант. Несколько разработчиков не могут добавлять новые значения к такому типу независимо друг от друга, так как их константы, возможно, будут конфликтовать друг с другом. Перечисления не обеспечивают легкого пути для преобразования констант перечисления в печатаемые строки или для перебора всех констант типа.

К сожалению, наиболее широко распространенный шаблон для перечислимых типов в Джава, имеет много общих недостатков с конструкцией перечислений в Си.

public class PlayingCard {      // ИгральныеКарты
   // ИСПОЛЬЗОВАНИЕ ЭТОГО ШИРОКО РАСПРОСТРАНЕНОГО ШАБЛОНА РЕДКО ОПРАВДАНО!
   public static final int SUIT_CLUBS    = 0;   // МАСТЬ_КРЕСТИ
   public static final int SUIT_DIAMONDS = 1;   // МАСТЬ_БУБИ
   public static final int SUIT_HEARTS   = 2;   // МАСТЬ_ЧЕРВИ
   public static final int SUIT_SPADES   = 3;   // МАСТЬ_ПИКИ
   ...
}

Вы можете также найти вариант этого шаблона, где вместо целых чисел используются строки. Этот вариант вообще не должен использоваться. Несмотря на то, что он обеспечивает печатаемые строки для констант, он может привести к проблемам с производительностью, так как он использует сравнение строк. Более того, он может привести неопытных пользователей к мысли использовать жестко закодированные в тексте клиентского кода строковые константы, а не соответствующие имена полей. Если такие жестко заданные строковые константы содержат ошибки в написании, то ошибка не будет обнаружена во время компиляции и приведет к ошибке времени выполнения.

К счастью, Джава предоставляет альтернативу, которая избегает всех недостатков целочисленного или строкового шаблонов и обеспечивает много других преимуществ. Эта альтернатива - типизированные перечисления (typesafe enum). К сожалению, этот шаблон не так широко известен. Основная идея проста: определение класса представляющего единичный элемент перечислимого типа и не предоставление для него публичного конструктора. Пользователю предоставляются только публичные статические финальные (public static final) поля, по одному для каждой константы перечислимого типа. Так как у клиента нет возможности для создания объектов такого типа, то и никогда не будут существовать объекты этого типа, кроме тех которые экспортированы через публичные статические финальные поля. Вот как этот шаблон выглядит в простейшем случае:

// Проектировочный шаблон типизированных перечислений
public class Suit {             // Масть
    private final String name;  // название
    private Suit(String name) { this.name = name; }
    public String toString()  { return name; }
    public static final Suit CLUBS = new Suit("clubs"); // КРЕСТИ
    public static final Suit DIAMONDS = new Suit("diamonds"); // БУБИ
    public static final Suit HEARTS = new Suit("hearts"); // ЧЕРВИ
    public static final Suit SPADES = new Suit("spades"); // ПИКИ
}

Как и следует из его названия, шаблон типизированных перечислений обеспечивает полную безопасность типов времени компиляции. Если вы объявите метод с параметром типа Suit, вы получаете гарантию, что любая не нулевая ссылка на объект переданная в качестве параметра является одной из четырех мастей. Любая попытка передать объект не правильного типа будет обнаружена во время компиляции, так же как и любая попытка присвоить результат вычисления выражения одного перечислимого типа переменой другого перечислимого типа. Несколько классов типизированных перечислений с одинаковыми именами констант перечисления мирно сосуществуют друг с другом, так как каждый класс имеет свое собственное пространство имен.

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

Так как типизированные перечисления являются полноправными классами, вы можете переопределить метод toString как показано выше, позволяя тем самым преобразовывать значения в печатаемые строки. Вы можете, если хотите, пойти еще дальше и интернационализировать типизированные перечисления с помощью стандартных средств Джава. Заметьте, что строковые имена используются в методе toString; они не используются при сравнениях двух значений, которое происходит при вызове метода equals, унаследованном от класса Object, в котором сравнивается только идентичность ссылок на объект.

Более того, вы можете расширить класс типизированного перечисления любым необходимым методом. Например, может быть полезно добавить в наш класс Suit (Масть) метод возвращающий цвет масти или метод возвращающий изображение масти. Класс может начаться как простое типизированное перечисление и эволюционизировать со временем в абстракцию со многими свойствами.

Так как к классам типизированных перечислений можно добавлять любые методы, то они могут реализовывать любые интерфейсы. Например, допустим, что вы хотите чтобы класс Suit реализовал интерфейс Comparable (Сравнимый), для того, что бы клиенты могли сортировать раздачу карт по мастям. Далее приводится немного измененный вариант первоначального шаблона, который добавляет такую способность. Статическая переменная nextOrdinal (следующаяПоПорядку) используется для присвоения порядкового номера каждому созданному экземпляру объекта. Эти порядковые числа используются методом compareTo (сравнитьС) для упорядочения экземпляров:

public class Suit implements Comparable {
    private final String name;
    // Порядковый номер следуещей созданой масти
    private static int nextOrdinal = 0;
    // Присвоение масти порядкового номера
    private final int ordinal = nextOrdinal++;
    private Suit(String name) { this.name = name; }
    public String toString()  { return this.name; }
    public int compareTo(Object o) {
        return ordinal - ((Suit)o).ordinal;
    }
    public static final Suit CLUBS    =        new Suit("clubs");
    public static final Suit DIAMONDS =        new Suit("diamonds");
    public static final Suit HEARTS   =        new Suit("hearts");
    public static final Suit SPADES   =        new Suit("spades");
}

Так как константы типизированных перечислений являются объектами, то вы можете использовать их с коллекциями (collections, обобщенные структуры данных). Например, допустим, вы хотите, что бы класс Suit экспортировал неизменяемый список мастей расположенных в стандартном порядке. Тогда просто добавьте в класс следующие два объявления полей:

  private static final Suit[] VALS = { CLUBS, DIAMONDS, HEARTS, SPADES };
   public static final List VALUES =
                Collections.unmodifiableList(Arrays.asList(VALS));

В отличие от простейшей формы типизированных перечислений, классы с порядковыми номерами с некоторым дополнительным усилием можно сделать сериализуемыми (serializable). Для этого, правда, не достаточно просто добавить в определение класса объявление о реализации интерфейса Serializable, нужно еще добавить метод readResolve:

   private Object readResolve() throws ObjectStreamException {
        return VALS[ordinal]; // Приведение к каноническому виду
    }

Этот метод, вызываемый автоматически системой сериализации, предохраняет константы от дублирования во время десериализации объектов. Это обеспечивает гарантию того, что только один объект представляет каждую константу перечисления, тем самым нам не нужно переопределять метод Object.equals. Без этой гарантии Object.equals будет сообщать не правильный отрицательный результат сравнения, когда две константы имеют одинаковые значения, но являются разными объектами. Заметьте, что метод readResolve ссылается на массив VALS, так что вы должны объявить этот массив, даже если вы не собираетесь экспортировать список VALUES.

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

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

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

Две техники, описанные в предыдущих параграфах, иллюстрируются в следующем классе типизированного перечисления. Класс Operation (Операция) представляет операции выполняемые простейшим четырехоперационым калькулятором. Вне пакета, в котором определен класс, все, что вы можете сделать с константами этого типа, это вызвать методы класса Object (toString, hashCode, equals и тому подобные). Внутри пакета вы, однако, можете выполнять арифметические действия представленные константами. Возможно пакет экспортирует некоторый высокоуровневый объект "калькулятор", который в свою очередь экспортирует один или более методов, которые принимают константы типа Operation в качестве параметра. Заметьте, что сам по себе класс Operation - абстрактный класс, содержащий один приватный-для-пакета абстрактный метод eval (вычислить), который выполняет соответствующую арифметическую операцию. Для каждой же константы определяется анонимный внутренний класс, для того, что бы каждая константа могла иметь свою версию метода eval.

public abstract class Operation {       // Операция
    private final String name;          // название
    Operation(String name) { this.name = name; }
    public String toString() { return this.name; }
    // Выполнение соответствующей арифметической операции
    abstract float eval(float x, float y);
    public static final Operation PLUS =        // ПЛЮС
            new Operation("+") {
                float eval(float x, float y) { return x + y; }
                            };
    public static final Operation MINUS =       // МИНУС
            new Operation("-") {
                float eval(float x, float y) { return x - y; }
                            };
    public static final Operation TIMES =       // УМНОЖЕНИЕ
            new Operation("*"){
                float eval(float x, float y) { return x * y; }
                            };
    public static final Operation DIVIDED_BY =  // ДЕЛЕНИЕ_НА
            new Operation("/") {
                float eval(float x, float y) { return x / y; }
                            };
}

Типизированные перечисления, в общем случае, сравнимы по производительности с обычными целочисленными константами перечислений. Два различных экземпляра класса типизированного перечисления никогда не представляют одно и то же значение, так что можно применять для проверки логического равенства сравнение ссылок, которое выполняется быстро. Клиенты типизированного перечисления могут использовать операцию == вместо вызова метода equals; результат будет гарантировано идентичен, а операция == будет выполнена уж наверное быстрее вызова метода.

Если типизированное перечисление общеупотребительно, то его лучше сделать классом верхнего уровня; если его использование привязано к специфичному классу верхнего уровня, то лучше сделать его публичным статическим вложенным (public static nested) классом этого класса верхнего уровня. Например, класс java.math.BigDecimal содержит набор целочисленных констант представляющих режимы округления для дробной части числа. Эти режимы округления обеспечивают полезную абстракцию, которая не привязанная фундаментально к классу BigDecimal, так что их можно заменить отдельным классом java.math.RoundingMode. Это будет стимулировать программистов, которым нужны режимы округления, использовать эти режимы округления, увеличивая тем самым единообразие среди API.

Базовый шаблон типизированных перечислений, как это показано на примере обоих реализаций перечисления Suit, фиксирован: пользователи не имеют возможности добавлять в перечисление новые элементы, так как класс не имеет доступных пользователю конструкторов. Это фактически делает класс финальным вне зависимости от того, объявлен ли он с модификатором доступа final или нет. Обычно это то, что вы хотите, но иногда вам нужно сделать класс типизированного перечисления расширяемым. Например, в случае если вы используете типизированное перечисление для представления форматов кодирования изображений и хотите дать третьим сторонам возможность добавлять поддержку новых форматов.

Для того, что бы сделать типизированное перечисление расширяемым просто обеспечьте защищенный (protected) конструктор. Другие затем могут расширять этот класс и добавлять новые константы. Вам не нужно бояться конфликтов констант перечисления так, как если бы вы использовали традиционный целочисленный шаблон. Расширяемый вариант шаблона типизированных перечислений использует преимущества пространства имен пакета Джава для создания "волшебно управляемого" пространства имен для расширяемого перечисления. Несколько организаций могут расширять перечисление без координации своих действий и их расширения никогда не будут конфликтовать между собой.

Простое добавление элемента в расширяемый перечислимый тип не означает, что новый элемент полностью поддерживается: методы, которые принимают элемент перечислимого типа в качестве параметра, должны справляться с ситуацией передачи элемента неизвестного программисту на момент написания метода. Конструкции с несколькими ветвями выполнения сомнительны даже для фиксированных перечислимых типов, а для расширяемых они просто смертельны, так как невозможно волшебным образом добавлять ветви каждый раз, когда программист расширяет тип. Один из путей преодоления этой проблемы - это создание типизированных перечислений со всеми методами необходимыми для описания поведения констант этого класса. Методы которые бесполезны для клиентов класса должны быть объявлены защищенными (protected), для сокрытия их от клиентов, но в то же время с оставленной для подклассов возможностью их переопределения. Если такой метод не имеет разумной стандартной реализации, то он может быть сделан так же и абстрактным.

Хорошо так же для класса расширяемого типизированного перечисления переопределить методы Object.equals и Object.hashCode своими финальными методами, которые вызывают соответствующие методы класса Object. Это позволяет быть уверенным, что никакой подкласс случайно не переопределит их. Это гарантирует, что все эквивалентные объекты перечислимого типа идентичны: a.equals(b) тогда и только тогда когда a==b .

   public final boolean equals(Object that) {
        return super.equals(that);
    }
    public final int hashCode() {
        return super.hashCode();
    }

Заметьте, что расширяемый вариант не совместим с вариантом реализующим интерфейс Comporable; если вы попытаетесь скомбинировать их, порядок следования элементов подкласса будет зависеть от порядка загрузки подклассов, который может различаться как от программы к программе, так и от запуска к запуску. Аналогично расширяемый вариант не совместим с сериализуемым вариантом.

Типизированные перечисления имеют несколько недостатков по сравнению с традиционным целочисленными перечислениями. Возможно, единственный серьезный недостаток это то, что константы типизированных перечислений не так удобно агрегировать в наборы (sets). С целочисленными перечислениями это обычно делается с помощью выбора в качестве значений для констант перечисления положительных степеней двойки и представление набора посредством операции "двоичного ИЛИ" над соответствующими константами:

   // Вариант битовых флагов
    // традиционного шаблона целочисленных перечислений
    public static final int SUIT_CLUBS    = 1;
    public static final int SUIT_DIAMONDS = 2;
    public static final int SUIT_HEARTS   = 4;
    public static final int SUIT_SPADES   = 8;
    hand.discard(SUIT_CLUBS | SUIT_SPADES);

Представление наборов констант перечислимых типов подобным образом кратко и очень быстро. Для наборов констант типизированных перечислений вы можете использовать реализацию структуры данных "набор" из стандартной библиотеки структур данных (Collections Framework), но это не так кратко и быстро в работе как в случае целочисленных перечислений:

   Set blackSuits = new HashSet();     // черные масти
    blackSuits.add( Suit.CLUBS);        // КРЕСТИ
    blackSuits.add( Suit.SPADES);       // ПИКИ
    hand.discard( blackSuits);

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

   hand.discard( new SuitSet( Suit.CLUBS, Suit.SPADES));

Менее чувствительный недостаток типизированных перечислений по сравнению с традиционными, это то, что первые не могут быть использованы в операторах switch, так как не являются целочисленными константами. Вместо этого вы можете использовать оператор if, как показано ниже.

   if (obj == Suit.CLUBS) {
        ...
    } else if (obj == Suit.DIAMONDS) {
        ...
    } else if (obj == Suit.HEARTS) {
        ...
    } else {
        // We know that obj == Suit.SPADES
        ...
    }

Оператор if может быть работает не так хорошо как оператор switch, но разница не такая сильная. Более того, необходимость в множественном ветвлении при использовании типизированных перечислений возникает редко, так как подобные ситуации решаются с помощью автоматической диспетчеризации вызовов методов.

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

Подводя итог, преимущества типизированных перечислений над целочисленными велики, и ни один из недостатков не существенен за исключением случаев, когда перечисление используется в основном в наборах элементов или в условиях ограниченных ресурсов. Поэтому типизированные перечисления должны быть первым что приходит на ум когда ситуация требует перечислимый тип. API, которые используют типизированные перечисления, более удобны использующему этот API программисту, чем целочисленные перечисления. Единственная причина, по которой они не так широко используются в API платформы Джава та, что этот шаблон проектирования не был известен, когда проектировались большинство этих API. Наконец, стоит еще раз повторить, что необходимость в перечислимых типах любого рода должна возникать редко при преимущественном использовании подклассов.

Замена указателей на функции классами и интерфейсами.

В Си имеются указатели на функции, которые позволяют программе сохранять и передавать возможность вызова определенной функции. Они обычно используются для возможности вызывающей стороне специализировать поведение вызываемой функции путем передачи ей в качестве параметра указателя на другую функцию, иногда называемую callback. Например, функция qsort из стандартной библиотеки Си принимает в качестве параметра указатель на функцию, которая используется для сравнения элементов сортируемого массива. Функция сравнения принимает два параметра, каждый из которых указатель на один из элементов массива. Она возвращает отрицательное значение, если элемент на который указывает первый параметр меньше элемента на который указывает второй параметр, ноль если эти элементы равны и положительное значение в остальных случаях. Различные виды сортировки могут быть получены передачей различных функций сортировки. Это пример проектировочного шаблона под название Стратегия – функция сравнения представляет стратегию сортировки элементов.

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

class StringLengthComparator {
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

Этот класс экспортирует один метод, который принимает две строки и возвращает отрицательное значение если первая строка короче второй, ноль, если строки равны по длине и положительное значение иначе. Этот метод является сравнителем, который сравнивает строки по их длине, вместо более распространенного лексиграфического сравнения. Ссылка на объект этого класса служит "указателем на функцию" этого сравнителя, позволяя вызывать ее над любыми парами строк. На языке сообщества разработчиков шаблонов проектирования, класс StringLengthComparator – конкретная стратегия для сравнения строк.

Класс StringLengthComparator - класс без состояния, что типично для классов реализующих конкретные стратегии. Он не имеет полей, поэтому все объекты этого класса функционально эквивалентны. Поэтому он может быть просто синглетоном для избежания не нужных расходов на создание объекта:

class StringLengthComparator {
    private StringLengthComparator () { }
    public static final StringLengthComparator
        INSTANCE = new StringLengthComparator();
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
}

Для передачи экземпляра StringLengthComparator в метод нам нужно иметь параметр соответствующего типа. Использовать в качестве такого типа сам StringLengthComparator не есть хорошая идея, так как тогда клиент не сможет передавать в метод другую стратегию. Вместо этого нам нужно определить обобщенный интерфейс Comparator (Сравнитель) и изменить StringLengthComparator так что он реализовывал этот интерфейс. На языке сообщества разработчиков шаблонов проектирования эта операция называется "определением абстрактной стратегии", которую реализовывает конкретная стратегия приведенная выше.

public interface Comparator {
    public int compare(Object o1, Object o2);
}

Это определение интерфейса Comparator взято из пакета java.util, но в нем нет ничего магического, вы могли бы написать такое сами. Так как операция сравнения применима не только к строкам, но и объектам других типов, метод в качестве параметров принимает объекты типа Object, а не String. Теперь нам нужно слегка изменить вышеприведенный класс StringLengthComparator, что бы он реализовывал интерфейс Comparable: параметры должны быть приведены к String, прежде чем над ними вызывать метод length.

Классы, реализующие конкретные стратегии, часто определяют как анонимные внутренние классы. Следующий код сортирует массив строк по их длине:

   Arrays.sort(stringArray, new Comparator () {
        public int compare(Object o1, Object o2) {
            String s1 = (String)o1;
            String s2 = (String)o2;
            return s1.length() - s2.length();
        }
    });

Потому что интерфейс абстрактной стратегии служит типом для всех объектов конкретных стратегий, класс конкретной стратегии не обязан быть публичным для экспорта объекта конкретной стратегии. Вместо этого класс "хозяин" может экспортировать публичное статическое поле (или статический метод-фабрику) типом которого является интерфейс абстрактной стратегии, а класс конкретной стратегии может быть приватным вложенным классом хозяина. В примере ниже, поименованный вложенный класс используется вместо анонимного внутреннего класса, что бы позволить классу конкретной стратегии реализовывать второй интерфейс - Serializable:

class Host {
    ...
    private static class StrLenCmp
         implements Comparator, Serializable {
        public int compare( Object o1, Object o2) {
            String s1 = (String)o1;
            String s2 = (String)o2;
            return s1.length() - s2.length();
        }
    }
    // Возвращаемый сравнитель сериализуем
    public static final Comparator
        STRING_LENGTH_COMPARATOR = new StrLenCmp();
}

Класс String использует этот проектировочный шаблон для экспорта через поле CASE_INSENSITIVE_ORDER сравнителя строк без учета регистра символов.

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

[an error occurred while processing this directive]
[an error occurred while processing this directive]Перевод на русский © Антон Блинков, 2001
< Вернуться на caйт :: Copyright © 1999 — 2010, IT • archiv.