Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Джош Блох

.pdf
Скачиваний:
57
Добавлен:
08.03.2016
Размер:
27.13 Mб
Скачать

Глава 6 Перечислимые ти п ы и аннотации

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

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

//Перечислимый тип, переключающийся на свое собственное

//значение, - спорно

public enum Operation {

PLUS, MINUS, TIMES, DIVIDE;

// Do the arithmetic op represented by this constant double apply(double x, double y) {

switch(this) {

case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y;

}

throw new AssertionError("Unknown op: “ + this);

}

}

Этот код работает, но он не очень хорош. Он не будет компили­ роваться без выражения throw, потому что конец метода технически может быть достигнут, хотя на самом деле он никогда не будет до­ стигнут [JLS, 14.21]. Что еще хуже, код довольно «хрупкий». Если вы добавите новую перечислимую константу, но забудете добавить соответствующий регистр к switch, перечислимый тип все равно от-

210

С т а т ь я 30

компилируется, но даст ошибку при выполнении, когда вы попытае­ тесь выполнить новую операцию.

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

от константы :

//Перечислимый тип с реализацией метода в зависимости от константы public enum Operation {

PLUS { double apply(double x, double y){return x + y;} }, MINUS { double apply(double x, double y){return x - y;} }, TIMES { double apply(double x, double y){return x * y; } }, DIVIDE { double apply(double x, double y){return x / y;} };

abstract double apply(double x, double y);

}

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

Реализация методов, привязанная к константам, может соче­ таться с привязанным к константам данным. Например, вот версия Operation, которая переопределяет toSt ring метод для возвращения символов, обычно связанных с операцией:

//Перечислимые типы с классами и данными, связанными

//с константами

public enum Operation { PLUS("+”) {

double apply(double x, double y) { return x + у; }

},

M I N U S ( ) {

211

Глава 6 Перечислимые ти п ы и аннотации

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

},

TIMES(“*”) {

double apply(double x, double y) { return x * y; }

},

DIVIDE(“/”) {

double apply(double x, double y) { return x / y; }

};

private final String symbol;

Operation(String symbol) { this.symbol = symbol; } @0verride public String toStringO { return symbol; } abstract double apply(double x, double y);

}

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

public static void main(St ring[] args) { double x = Double.parseDouble(args[0]); double у = Double.parseDouble(args[1]); for (Operation op : Operation.valuesO)

System.out.printf(“%f %s %f = %f%n”,

x, op, y, op.apply(x, y));

}

Запуск этой программы с аргументами командной строки 2 и 4 выводит на экран следующее:

2.000000 + 4.000000 = 6.000000

2.000000 - 4.000000 = -2.000000

2.000000 * 4.000000 = 8.000000

2.000000 / 4.000000 = 0.500000

У перечислимых типов есть автоматически генерируемые мето­ ды valueOf(String), которые переводят названия констант в сами константы. Если вы переопределите метод toString в перечислимом

212

С т а т ь я 30

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

// Реализация метода

fromString на перечислимом типе

private static final

Map<String, Operation> stringToEnum

= new HashMap<String,

Operation>();

static { // Initialize map from constant name to enum constant for (Operation op : valuesO) stringToEnum.put(op.toString(), op);

}

// Returns Operation for string, or null if string is invalid public static Operation fromString(String symbol) {

return stringToEnum.get(symbol);

}

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

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

213

Глава 6 Перечислимые типы и аннотации

вающий оплату рабочего на данный день, зная ставку зарплаты ра­ бочего (в час) и количество рабочих часов в тот день. При пятиднев­ ной неделе любое время сверх обычной смены должно генерировать повышенную оплату. С выражением switch легко выполнить расчет, применяя различные метки для различных случаев двух фрагментов кода. Для краткости код в данном примере использует double, но об­ ратите внимание, что double не является подходящим типом данных для приложений расчета оплаты труда (статья 48):

//Перечислимый тип, переключающийся на свое значение, чтобы

//сделать код общим, - спорно

enum PayrollDay {

MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

private static final int HOURS_PER_SHIFT = 8; double pay(double hoursWorked, double payRate) {

double basePay = hoursWorked * payRate;

double overtimePay; // Рассчитывает переработку switch(this) {

case SATURDAY: case SUNDAY:

overtimePay = hoursWorked * payRate / 2; break;

default: // Weekdays

overtimePay = hoursWorked <= HOURS_PER_SHIFT ?

0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;

}

return basePay + overtimePay;

}

}

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

214

С т а т ь я 30

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

Код можно уменьшить, заменив абстрактный метод overtimePay HaPayrollDay конкретным методом, который выполняет расчет пере­ работки в выходные дни. Но это приведет к тому же недостатку, что и выражение switch: если вы добавите еще день без переопределения метода overtimePay, то расчет будет вестись неправильно, как оплата в рабочие дни.

Что вам действительно нужно — это необходимость выбирать стратегию оплаты переработки каждый раз при добавлении перечис­ лимой константы. К счастью, есть способ, как этого достичь. Суть в том, чтобы переместить расчет оплаты переработки в закрытый вложенный перечислимый тип и передать экземпляр этого с т р а т е ­ гического перечислимого типа в конструктор перечислимому типу PayrollDay. Перечислимый тип PayrollDay затем передаст расчет оплаты переработки стратегическому перечислимому типу, избегая необходимости для выражения switch или использования реализации зависимого от констант метода в PayrollDay. Хотя этот шаблон менее краток, чем выражение switch, он безопаснее и более гибок:

// Стратегический перечислимый шаблон enum PayrollDay {

M0NDAY(РауТуре.WEEKDAY), TUESDAY(РауТуре.WEEKDAY), WEDNESDAY(РауТуре.WEEKDAY), THURSDAY(РауТуре.WEEKDAY), FRIDAY(РауТуре.WEEKDAY),

SATURDAY(РауТуре.WEEKEND), SUNDAY(РауТуре.WEEKEND); private final РауТуре payType;

PayrollDay(РауТуре payType) { this.payType - payType; } double pay(double hoursWorked, double payRate) {

215

Переключение на перечислимых типах хо­

Глава 6 Перечислимые типы и аннотации

return рауТуре.pay(hoursWorked, payRate);

}

// Стратегический перечислимый тип

private

enum

РауТуре

{

WEEKDAY {

 

 

 

double overtimePay(double hours, double payRate) {

 

 

return hours <= HOURS_PER_SHIFT ? 0 :

 

 

(hours

- HOURS_PER_SHIFT) * payRate / 2;

},

}

 

 

 

 

 

WEEKEND

{

 

 

double

overtimePay(double hours, double payRate) {

 

 

return

hours * payRate / 2;

}

};

private static final int HOURS_PER_SHIFT = 8;

abstract double overtimePay(double hrs, double payRate); double pay(double hoursWorked, double payRate) {

double basePay = hoursWorked * payRate;

return basePay + overtimePay(hoursWorked, payRate);

}

}

}

Е сли выражения switch перечислимых типов не являются хоро­ шим выбором для реализации реакций в зависимости от констант, чем же тогда они хороши?

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

// Переключение на перечислимом типе для имитации недостающего метода public static Operation inverse(Operation op) {

216

С тать я 30

switch(op) {

case PLUS: return Operation.MINUS; case MINUS: return Operation.PLUS; case TIMES: return Operation.DIVIDE; case DIVIDE: return Operation.TIMES;

default: throw new AssertionError(“Unknown op: “ + op);

}

}

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

Так когда же нам следует использовать перечислимые типы? В любом случае, когда вам требуется фиксированный набор кон­ стант. Конечно, это включает «натуральные перечислимые типы», такие как планеты, дни недели и шахматные фигуры. Но это также включает в себя другие наборы, для которых вы знаете все воз­ можные значения во время компиляции, такие как выбор пунктов меню, кодов операций и метки командной строки. Нет необходимо­ сти, чтобы набор констант в перечислимом типе оставался неизмен­ ным все время. Специально была добавлена возможность, чтобы позволить развиваться перечислимым типам в рамках двоичной со­ вместимости.

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

217

Глава 6 Перечислимые типы и аннотации

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

Используйте поля экземпляра вместо числовых значений

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

//Неверное использование порядкового расположения для выведения

//ассоциированного значения - НЕ ДЕЛАЙТЕ ТАК

public enum Ensemble {

SOLO, DUET, TRIO, QUARTET, QUINTET,

SEXTET, SEPTET, OCTET, NONET, DECTET;

public int numberOfMusicians() { return ordinalQ + 1; }

}

Хотя это и работает, его поддержка может стать кошмаром. Если поменять порядок констант, метод numberOfMusicians будет сломан. Если вы захотите добавить вторую перечислимую константу, связан­ ную со значением int, которую вы уже использовали, то вам сно­ ва не повезет. Например, может быть прекрасно, если вы захотите добавить константу, представляющую тройной квартер, состоящий из 12 музыкантов. Нет стандартного термина для ансамбля, состоя­ щего из 11 музыкантов, так что вам придется добавить константу для неиспользованного значения int (11). В лучшем случае это ужасно выглядит. Если много значений int не использованы, то это непрак­ тично.

218

С тать я 32

К счастью, есть простое решение этих проблем. Никогда не вы­ водите значение, связанное с перечислимыми типами, из их порядко­ вого положения; вместо этого храните их в поле экземпляра:

public enum Ensemble {

S0L0(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5), SEXTET(6), SEPTET(7), 0CTET(8), DOUBLE_QUARTET(8), N0NET(9), DECTET(10), TRIPLE_QUARTET(12);

private final int numberOfMusicians;

Ensemble(int size) { this.numberOfMusicians = size; }

public int numberOfMusicians() { return numberOfMusicians; }

}

Спецификация к Enum говорит об ordinal: «Большинство про­ граммистов вынуждены использовать этот метод. Он создан для ис­ пользования только структурами данных, на основе перечислимых типов общего назначения, таких как EnumSet и EnumMap». Если только вы не пишете такую структуру данных, лучше всего избегать исполь­ зования метода ordinal вообще.

Используйте EnumSet вместо битовых полей

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

// Перечислимые константы битовых полей - УСТАРЕЛО!

public class Text {

 

 

 

 

 

 

public

static

final

int

STYLE_B0LD = 1 «

0;

//1

 

public

static

final

int

STYLE_ITALIC = 1 «

1;

//2

public

static

final

int

STYLE_UNDERLINE = 1 «

2;

//4

public

static

final

int

STYLE_STRIKETHROUGH

= 1 «

3; //8

// Parameter

is bitwise

OR of zero or more

STYLE_ constants

219

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]