Джефф Фризен, JavaWorld | 12 ноября 2020
Использование в Java в качестве перечислений традиционных примитивных типов проблематично. Java 5 предоставила нам лучшую альтернативу в виде типизированных перечислений (enum). В этой статье я познакомлю вас с типизированными перечислениями, покажу вам, как объявить типизированное перечисление и использовать его в операторе switch, а также научу настраивать enum путем добавления данных и методов. И в конце статьи мы изучим класс java.lang.Enum<E extends Enum<E>>.
От перечислимых типов к типизированным перечислениям
Перечислимый тип определяется набором связанных констант в качестве его значений. Примерами могут быть: дни недели, стандартные направления компаса север / юг / восток / запад, номиналы монет и типы токенов лексического анализатора.
Перечислимые типы традиционно реализовывались как последовательности целочисленных констант, что демонстрируется следующим набором констант направления:
static final int DIR_NORTH = 0; static final int DIR_WEST = 1; static final int DIR_EAST = 2; static final int DIR_SOUTH = 3;
У такого подхода есть несколько проблем:
- Отсутствует типобезопасность: поскольку константа перечислимого типа является просто целым числом, любое целое число может быть указано там, где требуется константа. Кроме того, над этими константами можно выполнять сложение, вычитание и другие математические операции, например
(DIR_NORTH + DIR_EAST) / DIR_SOUTH), что бессмысленно. - Отсутствует пространство имен: константы перечисляемого типа должны иметь префикс какого-либо (надеюсь) уникального идентификатора (например,
DIR_), чтобы предотвратить конфликты с константами другого перечислимого типа. - Возможность сломать код: поскольку константы перечислимого типа компилируются в class-файлы, где хранятся их реальные значения (в пулах констант), изменение значения константы требует, чтобы эти class-файлы и те class-файлы приложений, которые от них зависят, были пересобраны. В противном случае поведение программы может стать непредсказуемым.
- Недостаток информации: при печати константы выводится ее целочисленное значение. Этот вывод ничего не говорит вам о том, что представляет собой целочисленное значение. Он даже не идентифицирует перечислимый тип, которому принадлежит константа.
Вы можете избежать проблем «отсутствия типобезопасности» и «недостатка информации», используя java.lang.String константы. Например, вы можете указать static final String DIR_NORTH = "NORTH";. Хотя значение константы более информативно, String константы по-прежнему страдают от «отсутствия пространства имен» и проблем с возможностью сломать код. Кроме того, вы не сможете сравнивать значение строк операторами == и != (которые в случае с объектами сравнивают ссылки).
Эти проблемы заставили разработчиков изобрести альтернативу на основе классов, известную как Typesafe Enum . Этот шаблон широко описывался и подвергался критике . Джошуа Блох представил шаблон в пункте 21 своего Руководства по эффективному языку программирования Java (Addison-Wesley, 2001) и отметил, что в нем есть некоторые проблемы; а именно, что неудобно агрегировать константы Typesafe Enum в наборы, и что константы в качестве перечислений не могут использоваться в операторе switch.
Рассмотрим следующий пример шаблона Typesafe Enum. Класс Suit показывает как вы могли бы использовать альтернативный вариант, основанный на классах для перечисления карточных мастей (трефы, бубны, червы, пика):
public final class Suit // Should not be able to subclass Suit.
{
public static final Suit CLUBS = new Suit();
public static final Suit DIAMONDS = new Suit();
public static final Suit HEARTS = new Suit();
public static final Suit SPADES = new Suit();
private Suit() {} // Should not be able to introduce additional constants.
}
Чтобы использовать этот класс, вы должны ввести переменную типа Suit и присвоить ее одной из Suit-констант следующим образом:
Suit suit = Suit.DIAMONDS;
Затем вы можете проверить suit в таком операторе switch:
switch (suit)
{
case Suit.CLUBS : System.out.println("clubs"); break;
case Suit.DIAMONDS: System.out.println("diamonds"); break;
case Suit.HEARTS : System.out.println("hearts"); break;
case Suit.SPADES : System.out.println("spades");
}
Однако, когда компилятор Java обнаруживает Suit.CLUBS, он сообщает об ошибке, в которой говорится, что требуется константное выражение. Вы можете попытаться решить проблему следующим образом:
switch (suit)
{
case CLUBS : System.out.println("clubs"); break;
case DIAMONDS: System.out.println("diamonds"); break;
case HEARTS : System.out.println("hearts"); break;
case SPADES : System.out.println("spades");
}
Но в таком случае, когда компилятор обнаруживает CLUBS, он сообщит об ошибке, в которой говорится, что ему не удалось найти символ. И даже если вы поместили Suit в пакет, импортировали пакет и статически импортировали эти константы, компилятор будет жаловаться на то, что он не может преобразовать Suit в int при обнаружении suit в switch(suit). А в каждом case компилятор сообщит, что требуется константное выражение.
Java не поддерживает шаблон Typesafe Enum с оператором switch. Зато поддерживает enum, который обладает всеми преимуществами этого шаблона и устраняет его проблемы, и enum уже поддерживается в switch.
Объявление enum и использование его в операторе switch
Простое объявление enum в коде Java выглядит так же, как его аналоги в языках C, C ++ и C #:
enum Direction { NORTH, WEST, EAST, SOUTH }
Здесь используется ключевое слово enum для объявления Direction в качестве типизированного перечисления (особый тип класса), в которое могут быть добавлены произвольные методы и реализованы произвольные интерфейсы. Константы enum NORTH, WEST, EAST и SOUTH являются экземплярами анонимного класса — потомка Direction.
Direction и другие типизированные перечисления являются потомками класса Enum<E extends Enum<E>> и наследуют различные методы, в том числе values(), toString() и compareTo() из этого класса. Мы рассмотрим класс Enum позже в этой статье.
Листинг 1 объявляет вышеупомянутое перечисление и использует его в конструкции switch. Он также показывает, как сравнить две константы enum, чтобы определить их порядок в объявлении.
Листинг 1: TEDemo.java(версия 1)
public class TEDemo
{
enum Direction { NORTH, WEST, EAST, SOUTH }
public static void main(String[] args)
{
for (int i = 0; i < Direction.values().length; i++)
{
Direction d = Direction.values()[i];
System.out.println(d);
switch (d)
{
case NORTH: System.out.println("Move north"); break;
case WEST : System.out.println("Move west"); break;
case EAST : System.out.println("Move east"); break;
case SOUTH: System.out.println("Move south"); break;
default : assert false: "unknown direction";
}
}
System.out.println(Direction.NORTH.compareTo(Direction.SOUTH));
}
}
В листинге 1 объявляется enum Direction и в цикле осуществляется проход по массиву всех его констант-объектов, который возвращается методом values(). Для каждого значения d оператор switch (расширенный для поддержки enum) выбирает соответствующую константу и выводит сообщение. (При этом ставить префикс перед константой enum, например, NORTH, не требуется.) Наконец, в листинге 1 выполняется сравнение:
Direction.NORTH.compareTo(Direction.SOUTH)
чтобы определить что объявлено раньше NORTH или SOUTH.
Скомпилируйте исходный код следующим образом:
javac TEDemo.java
Запустите скомпилированное приложение следующим образом:
java TEDemo
Вы должны увидеть следующий результат:
NORTH Move north WEST Move west EAST Move east SOUTH Move south -3
Выходные данные показывают, что унаследованный метод toString() возвращает имя константы перечисления, и что NORTH объявлен раньше чем SOUTH.
Добавление данных и поведения в типизированное перечисление
Вы можете добавлять данные (в виде полей) и поведение (в виде методов) в типизированное перечисление. Например, предположим, что вам нужно ввести enum для канадских монет, и этот класс должен предоставить средства для возврата количества пятицентовиков, десятицентовиков, двадцатипятицентовиков или долларов, содержащихся в произвольном количестве центов. В листинге 2 показано, как выполнить эту задачу.
Листинг 2: TEDemo.java (версия 2)
enum Coin
{
NICKEL(5), // constants must appear first
DIME(10),
QUARTER(25),
DOLLAR(100); // the semicolon is required
private final int valueInPennies;
Coin(int valueInPennies)
{
this.valueInPennies = valueInPennies;
}
int toCoins(int pennies)
{
return pennies / valueInPennies;
}
}
public class TEDemo
{
public static void main(String[] args)
{
if (args.length != 1)
{
System.err.println("usage: java TEDemo amountInPennies");
return;
}
int pennies = Integer.parseInt(args[0]);
for (int i = 0; i < Coin.values().length; i++)
System.out.println(pennies + " pennies contains " +
Coin.values()[i].toCoins(pennies) + " " +
Coin.values()[i].toString().toLowerCase() + "s");
}
}
В листинге 2 сначала объявляется enum Coin. Список параметризованных констант определяет четыре типа монет. Аргумент, передаваемый каждой константе, определяет количество центов в данной монете.
Аргумент, переданный каждой константе, фактически передается конструктору Coin(int valueInPennies), который сохраняет аргумент в поле экземпляра valuesInPennies. Доступ к этой переменной осуществляется из метода экземпляра toCoins(). На неё делится переданное в метод toCoins() количество центов, после чего возвращается сколько монет, определённых данной Coin константой, содержится в переданном количестве центов.
Теперь вы знаете, что можете объявлять поля, конструкторы и методы экземпляра в типизированном перечислении (enum). А значит, enum — это, по сути, особый вид Java-класса.
Метод main() класса TEDemo сначала проверяет, указан ли единственный аргумент командной строки. Этот аргумент преобразуется в целое число путем вызова метода parseInt() класса java.lang.Integer, который преобразует значение строкового аргумента в целое число (или выдает исключение при обнаружении недопустимого ввода). Я расскажу больше об Integer и других классах-обёртках в будущей статье Java 101.
Идём дальше. В методе main() перебираются Coin константы. Поскольку эти константы хранятся в массиве Coin[], мы получаем длину этого массива через Coin.values().length. В каждой итерации цикла мы получаем доступ к Coin константе через Coin.values()[i]. Для полученной константы вызывается toCoins() и toString(), что еще раз доказывает, что Coin это особый тип класса.
Скомпилируйте исходный код следующим образом:
javac TEDemo.java
Запустите скомпилированное приложение следующим образом:
Java TEDemo 198
Вы должны увидеть следующий результат:
198 pennies contains 39 nickels 198 pennies contains 19 dimes 198 pennies contains 7 quarters 198 pennies contains 1 dollars
Компилятор Java считает enum синтаксическим сахаром. При обнаружении объявления типизированного перечисления он генерирует класс, имя которого указано в объявлении. Этот класс является подклассом абстрактного Enum<E extends Enum<E>> класса, который служит базовым классом для всех типов перечислений.
Список формальных параметров класса Enum выглядит ужасно, но смысл не так уж и сложно понять. Например, Coin extends Enum<Coin> интерпретируется следующим образом:
- Любой подкласс
Enumдолжен предоставлять фактический аргумент типаEnum. Например, дляCoinэто будетEnum<Coin>. - Фактический аргумент типа должен быть подклассом
Enum. Например, дляCoinэто подклассEnum. - Подкласс
Enum(например,Coin) должен следовать идиоме, которая предоставляет собственное имя (Coin) в качестве фактического аргумента типа.
Исследуйте документацию Java по Enum, и вы обнаружите, что в нём переопределены методы java.lang.Object: clone(), equals(), finalize(), hashCode() и toString(). Все эти переопределённые методы, за исключением toString(), объявлены как final, так что их нельзя переопределить в подклассе:
clone()переопределяется, чтобы предотвратить клонирование констант, чтобы было не больше одной копии константы; в противном случае константы нельзя было бы сравнивать с помощью == и !=.equals()переопределяется для сравнения констант по ссылкам на них. Константы с одинаковыми названиями (==) должны иметь одинаковое содержимое (equals()), а разные названия подразумевают разное содержимое.finalize()переопределяется с целью запрета удаления объектов констант.hashCode()переопределяется за компанию с equals().toString()переопределяется, чтобы вернуть имя константы.
Enum также предоставляет свои собственные методы. В том числе final compareTo() (Enum реализует интерфейс java.lang.Comparable<T>), getDeclaringClass(), name() и ordinal():
compareTo()сравнивает текущую константу с константой, переданной в качестве аргумента, чтобы увидеть, какая константа предшествует другой константе в перечислении, и возвращает значение, указывающее порядковый номер. Этот метод позволяет отсортировать массив несортированных констант.getDeclaringClass()возвращает объект типаjava.lang.Class, соответствующийenumтекущей константы. Например, вenum Coin { PENNY, NICKEL, DIME, QUARTER}при вызовеCoin.PENNY.getDeclaringClass()будет возвращён объект типаClassдляCoin(я расскажу об этом в будущей статье Java 101)name()возвращает имя константы. Если toString() не переопределен нами для своих целей, он также вернёт имя константы.ordinal()возвращает целое число — порядковый номер начиная с нуля, которое определяет позицию константы в enum. Это целое число используется вcompareTo(). Например, в предыдущем примере выражениеDirection.NORTH.compareTo(Direction.SOUTH)вернуло -3, потому чтоNORTHимеет порядковый номер 0, аSOUTHимеет порядковый номер 3 и 0–3 (именно так здесь работаетcompareTo()) равно -3.
Enum также предоставляет метод public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) для возврата константы из указанного перечисления типов с указанным именем:
enumTypeидентифицируетClassобъекта перечисления, из которого следует вернуть константу.nameопределяет имя возвращаемой константы.
Например, Coin penny = Enum.valueOf(Coin.class, "PENNY"); присваивает переменной penny значение Coin константы с именем PENNY.
Наконец, Enum документация по Java не включает values() метод, потому что компилятор создает этот метод при создании файла класса enum.
Вам следует выработать привычку использовать типизированные перечисления (enum) вместо традиционных перечисляемых типов. Кроме того, возьмите за привычку использовать лямбды вместо анонимных внутренних классов. Про лямбды я расскажу в следующей статье Java 101.
Перевод Академии Progwards
