Джефф Фризен, 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