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


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

Амена констрvкций на языке С



 

 

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

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

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

 

3аменяйте структуру классом

 

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

 

 

encapsulate) свои данные в объекте, доступ к которому осуществляется только через его методы. Тем самым у разработчика появляется возможность менять внутреннее представление объекта (статья 12).

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

 

// Вырожденные классы, подобные этому,

// не должны быть открытыми!

class Point {

public float х;

public float у;

}

 

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

 

// Класс с инкапсулированной структурой

class Point {

private float х;

private float у;

public Point(float х, float у) {

this. х = х;

this.y = у;

}

public float getX() { return х; }

public float getY() { return у; }

public void setX(float х) { this.x = х; }

public void setY(float у) { this.y = у; }

}

 

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

 

 

 

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

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

 

 

3амеияйте объедииеиие иерархией классов

 

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

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

 

/* Явное объединение */

#include "math.h"

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;

/* Неверный тег */

 

}

}

 

Создатели языка программирования Java решили исключить конструкцию union, поскольку имеется лучший механизм определения типа данных, который можно ис­пользовать для представления объектов разных типов: создание подклассов. Явное объединение в действительности является лишь бледным подобием иерархии классов.

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

Далее, для каждого типа, который может быть представлен объединением, опре­делите неабстрактный подкласс корневого класса. В примере такими типами являются круг и прямоугольник. В каждый подкласс поместите те поля данных, которые ха­рактерны для соответствующего типа. Так, радиус является характеристикой круга,

 

 

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

 

abstract class Shape {

abstract double агеа(); }

class Circle extends Shape {

final double radius;

Circle(double radius) { this.radius = radius; }

double агеа() { 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 а геа() { return length * width; }

}

 

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

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

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

 

 

 

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

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

 

class Square extends Rectangle {

Square (double side) {

super(side, side);

}

double side() {

return length; // Возвращает длину или, что то же самое, ширину

}

}

 

Иерархия классов, представленная в этом примере, не является единственно

возможной для явного объединения. Данная иерархия содержит несколько конструк­торских решений, заслуживающих особого упоминания. для классов в иерархии, за исключением класса Square, доступ к полям обеспечивается непосредственно, а не че­рез методы доступа. Это делается для краткости, и было бы ошибкой, если бы классы были открытыми (статья 19). Указанные классы являются ·неизменяемыми, что не всегда возможно, но это обычно хорошее решение (статья 13).

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

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

 

union {

float f;

int bits;

}sleaze;

 

 

 

sleaze.f = 6.699е-41;

/* Помещает данные в одно из полей объединения ... */

ргiпtf("%х\n", sleaze.bits); /* ... и читает их из другого */

 

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

В пакете jаvа.lапg есть методы преобразования чисел с плавающей точкой в двоичную форму, однако в целях переносимости эти методы определены в соответст­вии со строго регламентированным двоичным представлением. Следующий фрагмент кода, который почти равнозначен предыдущему фрагменту на языке С, гарантирует, что программа будет всегда печатать один и тот же результат независимо от того, где она запущена:

 

Sуstет.оut.ргintln(

Intеgег.tоНехStгing(Flоаt.flоаtТolпtВits(6. 69ge-41f)));

 

3аменяйте конструкцию enum классом

 

Конструкция enum языка С отсутствует в языке программирования Jаvа. Обычно эта конструкция определяет тип, соответствующий перечислению (enumerated type), т. е. тип, допустимыми значениями которого являются константы из фиксированного набора. К сожалению, конструкция enum не очень хорошо определяет перечисления. Она лишь задает набор именованных целочисленных констант, не обеспечивая ни безопасности типов, ни минимального удобства использования. Так, в С допустимы не только записи:

 

typedef enum { FUJI, PIPPIN, GRANNY_SMITH } apple_t; /* Сорта яблок*/

typedef enum { NAVEL, TEMPLE, BLOOD } огапgе_t; /* Сорта апельсинов*/

огапgе_t myFavorite = PIPPIN; /* Путает яблоки и апельсины */

 

но и этот кошмар:

 

огаngе_t х = (FUJI - РIРРIN)/ТЕМРLЕ; /* Яблочный соус какой-то! */

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

typedef еnum { BLOOD, SWEAT, TEARS } fluid_t; /* Это не группа, а жидкости */

 

 

 

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

К сожалению, в языке программирования Jаvа большое распространение имеет шаблон перечислений, который, как показано ниже, перенимает указанные недостатки конструкции еnum из языка С:

// Шаблон перечисления·целых чисел (int enum)

­// весьма сомнительныйl

public class PlayingCard {

public static final int SUIT_CLUBS =0;

public static final int SUIT_DIAMONDS =1’

public static final iпt SUIT_HEARTS =2’

public static final int SUIT_SPADES =3;

 

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

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

 

// Шаблон typesafe enum

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"); // пики

}

Поскольку клиенты не могут создавать экземпляров данного класса или расши­рять его, не будет никаких объектов этого типа, за исключением тех, что доступны в полях public static final. И хотя класс даже не декларирован как final, расши­рять его невозможно: конструктор подкласса должен вызывать конструктор супер­класса, а такой конструктор ему недоступен.

Как и подразумевает название, шаблон typesafe enum обеспечивает безопасность типов уже на стадии компиляции. Декларируя метод с параметром типа Suit, вы имеете гарантию того, что любая переданная вам ссылка на объект, отличная от null, представляет одну из четырех правильных карточных мастей. Любая попытка пере­дать объект неправильного типа будет обнаружена на стадии компиляции так же, как и любая попытка присвоить результат вычислений, соответствующий одному перечис­лению, переменной, которая соответствует другому типу перечисления: Различные классы перечислений с одинаковыми названиями констант могут мирно сосущество­вать благодаря тому, что каждый класс имеет собственное пространство имен.

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

Поскольку перечисления типов - это вполне самостоятельные классы, допусти­мо переопределение метода toString таким образом, чтобы можно было преобразовы­вать значения в печатные строки. При желании вы сможете сделать еще один шаг в этом направлении и с помощью стандартных средств связать класс перечисления с региональными настройками. Заметим, что названия строк используются лишь в методе toString, при проверке равенства они не применяются, поскольку реализация метода equals, унаследованная от класса Object, выполняет сравнение, проверяя ра­венство ссылок.

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

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

 

 

 

 

 

карт в бридже по мастям. Представим слегка видоизмененный первоначальный шаб­лон, который поддерживает такой трюк. Статическая переменная nextOrdinal исполь­зуется для того, чтобы каждому экземпляру класса в момент его создания назначать порядковый номер. Эти номера применяются в методе compareTo для упорядочения экземпляров.

// Перечисление, использующее порядковые номера

public class Suit implements Comaprable {

private final String name;

// Порядковый номер следующей масти

private static int nextOrdinal = 0;

// Назначение порядкового номера данной масти

private final int ordinal = nextOrdinal++;

private Suit(String паmе) { this.паmе = паmе; }

public Stгiпg tоStгiпg() {return паmе; }

 

public iпt compareTo(Object о) {

геturn огdinаl – ((Suit)О).огdiпаl;

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 передавал неизменяемый перечень мастей в обычном порядке. Достаточно добавить в этот класс следующие две декларации полей:

 

private static fiпаl Suit[] PRIVATE_VALUES =

{ CLUBS, DIAMONDS, HEARTS, SPADES };

public static final List VALUES = Соllесtiопs.unmоdifiаblеList(Аггауs.аsList(РRIVAТЕ_VALUES));

В отличие от простейшего шаблона typesafe enum, классы представленной формы, использующей порядковые номера, можно сделать сериализуемыми (serializabIe) (см. главу 10), при ложи в для этого минимум усилий. Недостаточно добавить в де­кларацию класса слова imрlеmепts Serializab'le, нужно еще предоставить метод readResolve (статья 57):

private Object readResolve() throws ObjectStreamExceptin {

return PRIVATE_VALUES[ordinal]; // Канонизация

}

 

 

 

 

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

Полученный в результате класс не вполне надежен: конструкторы любых вновь добавленных значений должны ставиться после уже имевшихся с тем, чтобы ранее сериализованные экземпляры класса при десериализации не поменяли свое значение. Это происходит потому, что в сериализованной форме (статья 55) для константы пе­речисления хранится лишь ее порядковый номер. И если у константы перечисления поменяется порядковый номер, то константа, сериализованная с прежним номером, при десериализации получит новое значение.

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

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

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

 

 

 

 

 

 

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

// Перечисление типов, схемы поведения которого

// закреплены за константами

publiC abstract class Operation {

private final String name;

Operation(String name) { this.name = пате; }

public String toString() { return this.name; }

//Выполняет арифметическую операцию,

// представленную указанной константой

abstract double eval(double х, double у);

public static final Operation PLUS = new Operation("+") {

double eval(double х, double у) { return х + у; }

} ;

public static final Operation MINUS = new Operation("-") {

double eval(double х, double у) { return х - у; }

} ;

public static final Operation TIMES : new Operation("*") {

double eval(double х, double у) { return х * у; }

} ;

public static final Operation DIVIDED_BY = new Operation("/") {

double eval(double х, double у) { return х / у; }

} ;

Вообще говоря, по производительности перечисления типов сравнимы с перечис­лениями целых констант. Два разных экземпляра для класса перечисления typesafe еnиm никогда не смогут представить клиенту одно и то же значение, а потому для про­Верки логического равенства используется быстрая проверка тождественности ссылок. Клиенты класса перечисления typesafe еnиm могут использовать оператор == вместо метода equals. Гарантируется, что результаты будут те же самые, а сам оператор ==, возможно, будет работать быстрее.

Если класс перечисления используется широко, его следует сделать классом верх­Него уровня. Если работа с перечислением связана с определенным классом верхнего уровня, перечисление следует сделать статическим классом-членом указанного класса верхнего уровня (статья 18). Например, класс java. math. BigDecimal содержит набор

 

 

 

 

констант перечисления типа int, соответствующих различным способам округления дробной части числа (roиnding modes). Эти способы округления образуют полезную абстракцию, которая в сущности не связана с классом BigDecimal. Реализовать их лучше в самостоятельном классе j ava. math. RoundingMode. В результате любой про­граммист, которому понадобились различные режимы округления, мог бы воспользо­вался этой абстракцией, что привело бы к лучшей согласованности между различными

API.

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

Для того чтобы сделать класс перечисления расширяемым, создайте защищенный конструктор. Тогда другие разработчики смогут расширять этот класс и дополнять но­выми константами свои подклассы. Вам не нужно беспокоиться о конфликтах между константами перечисления, как в случае применения шаблона int епит. Расширяемый вариант шаблона typesafe епит использует преимущества собственного пространства имен пакета с тем, чтобы для расширяемого перечисления создать "магически управ­ляемое" пространство имен. Различные группы разработчиков могут расширять это перечисление, ничего не зная друг о друге,- конфликта между их расширениями не возникнет.

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

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

В расширяемом классе перечисления полезно пере определить методы equals и hashCode новыми окончательными методами, которые обращаются к методам из класса Object. Тем самым гарантируется, что ни в одном подклассе эти методы не

 

 

будут случайно переопределены, благодаря чему все равные объекты типа перечисле­ния будут к тому же идентичны (a.equals(b) возвращает true тогда и только тогда, когда а == Ь).

// Методы, защищенные от переопределения

publiс final boolean equals(Object that) {

return super.equals(that);

}

public final int hashCode() {

return super.hashCode();

}

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

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

// Сериализуемый и расширяемый класс перечисления

public abstract class Operation implements Serializable {

private final transient String name;

protected Operation(String name) { this.name = name; }

 

public static Operation PLUS = new Operation("+") {

protected double eval(double х, double у) { return х+у; }

} ;

public static Operation MINUS = new Operation("-") {

protected double eval(double х, double у) { return х-у; }

} ;

public static Operation TIMES = new Operation("*") {

protected dQuble eval(double х, double у) { return х*у; }

} ;

public static Operation DIVIDE = new Operation("/") {

protected double eval(double х, double у) { return x/y; }

} ;

 

 

 

// Выполнение арифметической операции,

// представленной данной константой

protected abstract double eval(double х, double у);

public String toString() { return this.name; }

// Препятствует переопределению в подклассах

// метода Object.equals

public final boolean equals(Object that) {

return super.equals(that);

}

public final int hashCode() {

return super.hashCode();

}

// Следующие четыре декларации необходимы для сериализации

private static int nextOrdinal = о;

private final int ordinal = nextOrdinal++;

private static final Operation[] VALUES =

{ PLUS. MINUS, TIMES, DIVIDE};

Object readResolve() throws ObjectStreamException {

return VALUES[ordinal]; // Канонизация

}

}

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

// Подкласс расширяемого сериализуемого перечисления

abstract class ExtendedOperation extends Operation {

 

ExtendedOperation(String name) { super(name); }

public static Operation LOG = new ExtendedOperation("log") {

protected double eval(double х, double у) {

return Math.log(y) / Math.log(x);

} ;

public static Operation ЕХР = new ExtendedOperation("exp") {

protected double eval(double х, double у) {

return Math.pow(x, у);

}

} ;

 

 

 

// Следующие четыре декларации необходимы для сериализации

private static int nextOrdinal = 0;

private final int ordinal = nextOrdinal++;

private static final Operation[] VALUES = { LOG, ЕХР };

Object readResolve() throws ObjectStreamException {

return VALUES[ordinal]; // Канонизация

}

}

 

Заметим, что в представленных классах методы readResolve показаны как доступные в пределах пакета, а не закрытые. Это необходимо потому, что экземпляры классов Operation и ExtendedOperation фактически являются экземплярами анонимных под­классов, а потому закрытые методы readReasolve были бы бесполезны (статья 57).

Шаблон typesafe епит, по сравнению с шаблоном int enum, имеет несколько недостатков. По-видимому, единственным серьезным его недостатком является то, что он не так удобен для объединения констант перечисления в наборы. В перечислениях целых чисел для констант традиционно выбираются значения в виде различных не­отрицательных степеней числа два, сам же набор представляется как побитовое ОR соответствующих констант:

 

// Вариант шаблона int впит с битовыми флажками

public static final int SUIT_CLUBS =0;

public static final int SUIT_DIAMONDS =1’

public static final iпt SUIT_HEARTS =2’

public static final int SUIT_SPADES =3;

public static final int SUIT_BLACK ; SUIT_CLUBS I SUIT_SPADES;

 

Набор констант перечисления, представленный таким образом, является крат­ким и чрезвычайно быстрым. Для набора констант перечисления typesafe enum вы мо­жете использовать универсальную реализацию набора, заимствованную из Collections Framework, однако такое решение не является ни кратким, ни быстрым:

Set blackSuits ; new HashSet(); blackSuits.add(Suit.CLUBS); blackSuits.add(Suit.SPADES);

Хотя наборы констант перечисления typesafe eum вряд ли можно сделать такими же компактными и быстрыми, как наборы констант перечисления int enum, указанное неравенство можно уменьшить путем специальной реализации набора Set, которая обслуживает элементы только определенного типа, а для самого набора использует Внутреннее представление в виде двоичного вектора. Такой набор лучше реализовы­вать в том же пакете, где описывается тип его элементов. Это позволяет через поля или методы, доступные только в пределах пакета, получать доступ к битовому значению, которое соответствует внутреннему представлению каждой константы в перечис­лении. Имеет смысл создать открытые конструкторы, которые в качестве параметров

 

 

 

принимают короткие последовательности элементов, что делает возможным примене­ние идиом следующего типа:

 

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

 

Небольшой недостаток констант перечисления typesafe enums, по сравнению с пе­речислением int enum, заключается в том, что для них нельзя использовать оператор switch, поскольку они не являются целочисленными. Вместо этого вы применяете оператор if, например, следующим образом:

if (suit == Suit.CLUBS) {

}else if (suit == Suit.DIAMONDS) {

}else if (suit == Suit.HEARTS) {

}else if (suit == Suit.SPADES) {

}else {

throw new NullPointerException("Null Suit"); //suit = = null

}

 

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

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

Подведем итоги. Преимущества перечислений typesafe enum перед перечисления­ми int еenum огромны, и ни один из недостатков не кажется непреодолимым, за исклю­чением случая, когда перечисления применяются прежде всего как элемент набора либо в среде, серьезно ограниченной в ресурсах. Таким образом, когда обстоятель­ства требуют введения перечисления, на ум сразу же должен приходить шаблон typesafe епит. API, использующие перечисления typesafe enum, гораздо удобнее для программиста, чем API, ориентированные на перечисления int enum. Единственная причина, по которой шаблон typesafe enum не применяется более интенсивно в интер­фейсах АРI для платформы Java, заключается в том, что в то время, когда писались многие из этих API, данный шаблон еще не был известен. Наконец, стоит повторить еще раз, что потребность в перечислениях любого вида должна возникать сравни­тельно редко, поскольку большинство этих типов после создания подклассов стали устаревшими (статья 20).

 

 

 

 




Поиск по сайту:







©2015-2020 mykonspekts.ru Все права принадлежат авторам размещенных материалов.