Джош Блох
.pdfГлава 6 * Перечислимые типы и аннотации
public void applyStyles(int styles) { ... }
}
Это представление позволяет вам использовать побитовую опе рацию O R для объединения нескольких констант в набор, известный как битовое поле:
text. applyStyles(STYLE_BOLD | STYLEJTALIC);
Представления битовых полей позволяют эффективно выполнять встроенные операции, такие как объединение и пересечение, исполь зуя побитовую арифметику. Но у битового поля есть все недостатки перечислимых констант int, и даже больше. Битовое поле труднее интерпретировать, чем простую перечислимую константу int, если она распечатана в виде числа. Также нет простого способа итерации для всех элементов, представленных битовым полем.
Некоторые программисты, предпочитающие использовать пере числимые типы вместо констант int, все еще используют битовые поля, если им необходимо обойти наборы констант. Нет нужды так делать — есть лучшая альтернатива. Пакет java .utils дает нам класс EnumSet для эффективного представления набора значений, извлечен ных из одного перечислимого типа. Этот класс реализует интерфейс Set, давая вам все богатство, безопасность типов и возможность вза имодействия, которые вы можете получить при любой другой реали зации Set. Но внутренне каждый EnumSet представлен как битовый вектор. Если у основного перечислимого типа 64 или менее элемен тов — у большинства так, — то весь EnumSet представлен одним long, так что его производительность сравнима с производительностью битового поля. Групповые операции, такие как RemoveAll и RetainAll, реализуются с использованием побитовой арифметики, так как если бы вы делали это вручную для битовых полей. Но вы теперь избав лены от сложности и ошибок ручной манипуляции с битами: EnumSet выполняет эту сложную работу для вас.
Вот как выглядит предыдущий пример, если его изменить для использования перечислимых типов вместо битовых полей. Он коро че, яснее и безопаснее:
2 20
С тать я 33
// EnumSet - современная замена битовым полям public class Text {
public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH } // Any Set could be passed in, but EnumSet is clearly best public void applyStyles(Set<Style> styles) { ... }
}
Это клиентский код, который передает экземпляр EnumSet мето ду applyStyles. EnumSet дает богатый набор методов статической ге нерации для упрощения создания наборов, один из которых приведен в данном коде:
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
Обратите внимание, что метод applyStyles отдает предпочте ние Set<Style>, а не EnumSet<Style>. Хотя кажется, что все клиен ты должны передавать методу EnumSet, хорошей практикой является принятие типа интерфейса вместо типа реализации. Это дает возмож ность необычному клиенту передать в какой-либо другой реализации Set и не обладает какими-либо недостатками.
Подведем итоги. Поскольку перечислимые типы будут использо ваться в наборах, нет необходимости представлять их битовыми поля ми. Класс EnumSet объединяет в себе краткость и производительность битовых полей со всеми преимуществами перечислимых типов, опи санных в статье 30. Есть один недостаток EnumSet — невозможность создать неизменяемый EnumSet (по крайней мере, в версии 1.6), но ско рее всего он будет исправлен в будущих версиях. А пока вы можете по
местить EnumSet в оболочку, с помощью Collections. unmodifiableSet,
но при этом и краткость и производительность пострадают.
Используйте EnumMap вместо порядкового индексирования
Вам может попасться код, использующий метод ordinal (ста тья 31) для индексирования внутри массива. Например, рассмо
221
Глава б • Перечислимые типы и аннотации
трим этот простой класс, который должен представлять кулинар ную траву:
class Herb {
enum Type { ANNUAL, PERENNIAL, BIENNIAL } final String name;
final Type type;
Herb(String name, Type type) { this.name = name; this.type = type;
}
@0verride public String toStringO { return name;
}
}
Теперь предположим, что у вас есть массив трав, представляю щий растения в саду, и вы хотите составить список этих растений, организованный по типу (однолетние, двухлетние или многолетние). Для того чтобы это сделать, вы создаете три набора, по одному для каждого типа, и выполняете итерацию по саду, помещая каждую тра ву в соответствующий набор. Некоторые программисты сделают это помещая наборы в массив, индексированный по типу:
//Using Используется ordinal() для индексирования массива -
//НЕ ДЕЛАЙТЕ ТАК!
Неrb[ ] garden = .. . ;
Set<Herb>[] herbsByType = // Indexed by Herb.Type.ordinalQ (Set<Herb>[]) new Set[Herb.Type.values().length];
for (int i = 0; i < herbsByType.length; i++) herbsByType[i] = new HashSet<Herb>();
for (Herb h : garden) herbsByType[h.type.ordinal()].add(h); // Выводит результаты
for (int i = 0; i < herbsByType.length; i++) { System.out.printf("%s: %s%n”, Herb.Type.values()[i], herbsByType[i]);
}
222
С татья 33
Данный прием работает, но в нем много проблем. Так как мас сив несовместим с средствами обобщенного программирования (статья 25), программе потребуется необработанная передача и она не будет чисто компилироваться. Поскольку массиву неизвестно, что представляет собой его индекс, то необходимо пометить резуль тат вручную. Но самая серьезная проблема — когда вы получите доступ к массиву, индексированному порядковым перечислимым типом, то на вас ляжет ответственность использования правильного значения int; int не обеспечивает безопасность перечислимых типов. Если вы используете неверное значение, программа будет тихо де лать не то, что нужно, или — если вам повезет — выдаст ошибку ArraylndexOutOfBoundsException.
К счастью, есть намного более удачный способ добиться того же эффекта. Массив эффективно выполняет роль карты меж ду перечислимым типом и значением, так что можно использо вать Мар. Заметим, что есть быстрая реализация Мар, создан ная для использования с перечислимыми ключами, известная как java. util. EnumMap. Вот как выглядит программа, переписанная для использования EnumMap:
// Использование EnumMap для связи данных с перечислимым типом
MapcHerb.Type, Set<Herb>> herbsByType =
new EnumMapCHerb. Type, Set<Herb»(Herb.Type, class); for (Herb.Type t : Herb.Type.values()) herbsByType.put(t, new HashSet<Herb>());
for (Herb h : garden) herbsByType.get(h.type).add(h); System.out.println(herbsByType);
Эта программа короче, чище, безопаснее и по скорости сравнима с оригинальной версией. Здесь нет небезопасных передач, нет необ ходимости вручную помечать результат, поскольку ключи карты — это перечислимые типы, которые знают, как перевести самих себя в печатаемые строки, и нет вероятности ошибок в расчете индексов массивов. Причина, почему EnumMap сравним по скорости с массивом,
223
Глава 6 • Перечислимые типы и аннотации
индексированным по порядку, заключается в том, что EnumMap ис пользует этот массив внутренне. Но он скрывает детали этой реали зации от программиста, объединяя богатство и безопасность типов, которые свойственны Мар со скоростью массивов. Обратите внима ние, что конструктор EnumMap берет объект Class ключевого типа: это маркер связанного типа, который обеспечивает информацию о вы полнении обобщенных типов (статья 29).
Вы можете увидеть массив индексов массива (дважды) по по рядку, используемый для представления карты из двух значений пе речислимых типов. Например, эта программа использует такой мас сив для связывания двух фаз с переходом в фазу (жидкое состояние в твердое называется замерзанием, жидкое в газообразное — кипе нием и т.д.):
//Использует ordinal() для индексирования массива в массив -
//НЕ ДЕЛАЙТЕ ТАК!
public enum Phase { SOLID, |
LIQUID, GAS; |
public enum Transition |
{ |
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; |
|
// Строки индексируются с помощью src-ordinal, столбцы |
|
//сс помощью dst-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME |
}, |
|
{ FREEZE, |
null, BOIL |
}, |
{ DEPOSIT, |
CONDENSE, |
null } |
};
// Возвращает переход из одного состояния в другое public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
Эта программа работает и может выглядеть элегантно, но это впечатление обманчиво. Как и в более простом примере с травами в саду, компилятор не знает отношений между порядковыми номе
224
С татья 33
рами и индексами массива. Если вы сделаете ошибку в таблице пе рехода или забудете обновить ее при изменении перечислимых ти пов Phase или Phase.Т ransition, то ваша программа даст ошибку при выполнении. Ошибка может быть в виде ArrayOutOfBoundsException, NullPointer Except ion или, что еще хуже, будет тихо работать некор ректно. И размер таблицы будет квадратичен количеству фаз, даже если количество ненулевых входных значений будет меньше.
И опять же, вы можете сделать все гораздо лучше с помощью EnumMap. Поскольку каждый переход из одной фазы в другую индек сируется парой перечислимых типов фаз, то лучше всего представить отношения в виде карты от одного перечислимого типа (исходная фаза) к карте другого (фаза, в которую осуществляется переход) и к результату (сам переход). Две фазы, связанные с переходом, лучше всего записывать, связывая данные с перечислимыми типами перехода из фазы в фазу, который затем используется для инициали зации вложенного класса EnumMap:
//Использование вложенного класса EnumMap для ассоциации данных
//с парами перечислимых типов
public enum Phase { SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
B0IL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID); private final Phase src;
private final Phase dst; Transition(Phase src, Phase dst) {
this.src = src; this.dst = dst;
}
// Инициализирует карту перехода из фазы в фазу
private static final Map<Phase, Map<Phase,Transition» m = new EnumMap<Phase, Map<Phase,Transition»(Phase. class); static {
225
Глава 6 • Перечислимые типы и аннотации
for (Phase р : Phase.values())
m.put(p,new EnumMap<Phase,Transition^Phase.class)); for (Transition trans : Transition.valuesO)
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) { return in.get (src).get(dst);
}
}
}
Код для инициализации фазы перехода может выглядеть сложнее, но он не так уж плох. Типами карты являются Map<Phase, Map<Phase, Transition», что означает «карта от исходной фазы к фазе назначе ния и переходу». Первый цикл в блоке статической инициализации инициализирует наружную карту, содержащую три пустые внутрен ние карты. Второй цикл в блоке инициализирует внутренние карты, используя информацию об источнике и назначении, которое дается с помощью константы перехода состояния.
Теперь предположим, что вы хотите добавить новую фазу в си стему: плазму или ионизированный газ. Есть только два перехода, связанные с этой фазой: ионизация, которая преобразует газ в плаз му, и деионизация, которая обратно возвращает плазму в газ. Для об новления программы на основе массива вам придется добавить одну новую константу к Phase или две к Phase. Т ransition и заменить ори гинальный массив из массива из девяти элементов на новую версию из шестнадцати элементов. Если вы добавите слишком много или слишком мало элементов в массив или поместите элементы не в том порядке, то вам не повезет: программа откомпилируется, но выведет ошибку при выполнении. Для обновления версии на основе EnumMap все, что вам надо сделать, - это добавить списк фаз PLASMA а также
IONIZE (GAS, PLASMA) и DEIONIZE (PLASMA, GAS) к списку переходов.
Программа обо всем остальном сама позаботится, и у вас не будет возможности сделать ошибку. Внутренне карта карт реализуется как массив массивов, так что вы не много теряете в плане затрат и вре-
226
С т а т ь я 34
мени выполнения, взамен получая большую ясность, безопасность и простоту поддержки.
Подведем итоги. Редко бывает ситуация, когда для индексиро вания массивов используются порядковые номера: вместо них надо использовать EnumMap. Если взаимоотношения, которые вы хотите представить, находятся во многих измерениях, используйте ЕпипМар<..., ЕпиплМарС..» . Это особый случай общего принципа, в кото ром программисты приложений должны использовать Enum. ordinal (статья 31).
Имитируйте расширяемые перечислимые типы
с помощью интерфейсов
Во всех отношениях перечислимые типы превосходят безопас ные перечислимые шаблоны, описанные в первой редакции этой книги [BlochOl]. Однако при всем этом имеется одно исключение, оно касается расширяемости, которая была возможна в оригиналь ном шаблоне, но не поддерживается конструкцией языка. Другими словами, используя шаблон, можно было сделать так, чтобы один пе речислимый тип расширял другой, используя те же средства языка. Это не случайно. По большей части расширение перечислимых типов оказывается плохой идеей. Становится запутанным то, что элементы расширяемого типа являются экземплярами основного типа, а не на оборот. Это не очень хорошая идея — перечислять все элементы ос новного типа и его расширения. Наконец, расширяемость усложнит многие аспекты дизайна и реализации.
Ранее отмечалось, что есть случай, когда следует использовать расширяемые перечислимые типы, называемые операционными ко дами. Операционный код — это перечислимый тип, элементы ко торого представляют операции на некой «машине» типа Operation в статье 30, который представляет функции простого калькулятора. Иногда желательно позволить пользователям API предоставлять
227
Глава 6 • Перечислимые типы и аннотации
свои собственные операции, эффективно расширяя набор операций, предоставляемых API.
К счастью, есть прекрасный способ достичь такого эффекта с ис пользованием перечислимых типов. Основная идея — это извлечь преимущества из факта, что перечислимые типы могут реализовы вать произвольные интерфейсы, определяя интерфейс для типа опе рационных кодов и перечислимого типа, являющегося стандартной реализацией интерфейса, Например, вот пример расширяемой версии типа Operation статьи 30:
//Имитированный расширяемый перечислимый тип с использованием
//интерфейса
public interface Operation { double apply(double x, double y);
}
public enum BasicOperation implements Operation { PLUS(“+”) {
public double apply(double x, double y) { return x + y; }
},
M I N U S ( ) {
public double apply(double x, double y) { return x - y; }
},
TIMES("*”) {
public double apply(double x, double y) { return x * y; }
},
DIVIDEO/») {
public double apply(double x, double y) { return x / y; }
};
private final String symbol; BasicOperation(String symbol) {
this.symbol = symbol;
}
@0verride public String toStringO { return symbol;
}
}
228
С татья 34
Хотя перечислимый тип (BasicOperaion) и не является расши ряемым, тип интерфейса (Operation) расширяем, и он является ти пом интерфейса, который используется для представления операций в API. Вы можете определить другой перечислимый тип, который реализует этот интерфейс, и использовать экземпляры этого ново го типа вместо основного типа. Например, предположим, вы хотите определить расширение для операционного типа, о котором шла речь выше, состоящим из возведения в степень и остальных операций. Все, что вам нужно сделать, — это написать перечислимый тип для реализации интерфейса Operation:
// Имитированный перечислимый тип расширения extension public enum ExtendedOperation implements Operation {
E X P ( ) {
public double apply(double x, double y) { return Math.pow(x, y);
}
},
REMAINDER “%”) {
public double apply(double x, double y) { return x % y;
}
};
private final String symbol; ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@0verride public String toStringO { return symbol;
}
}
Вы можете использовать новые операции там же, где вы могли бы использовать основные операции, при условии, что A PI написа ны так, чтобы они могли принимать тип интерфейса (Operation), а не реализацию Обратите внимание, что вам не нужно
229
