Джош Блох
.pdfГлава 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