Перечисление (![Семь осенних листьев на веревке: заглавная картинка статьи о перечислениях в Java]()
enum) в Java это особый вид класса, у которого заранее задан конечный набор именованных экземпляров: enum Season { WINTER, SPRING, SUMMER, AUTUMN }. Каждый элемент это статически доступный объект, поэтому их сравнивают через ==, а сам класс компилятор молча наследует от java.lang.Enum и добавляет туда методы values() и valueOf(). Появился enum в Java 5.Кратко
enumзадает ограниченный набор значений: дни недели, времена года, статусы заказа. Компилятор превращает объявление в класс, наследникjava.lang.Enum.- Элементы перечисления это статические экземпляры этого класса, каждый в единственном числе, поэтому сравнивать их можно через
==, а не только черезequals(). - Из
java.lang.Enumнаследуютсяname(),toString(),ordinal(),compareTo(); методыvalues()иvalueOf(String)дописывает компилятор. valueOf()бросаетIllegalArgumentException, если такого элемента нет, иNullPointerException, если передатьnull.- В
enumможно добавлять поля, конструкторы и методы, в том числе свой метод у каждого элемента: тогда компилятор создает вложенные классы-наследники (Type$1,Type$2и так далее). - Чего
enumне умеет: наследоваться от другого класса и быть обобщенным (enum Type<T>не скомпилируется), зато может реализовывать интерфейсы.
enum). В Java перечисление появилось не сразу. Специализированная языковая конструкция enum была введена начиная с версии 1.5. До этого момента программисты использовали другие методы для реализации перечислений.
Конструкция enum
Начнем с примера. Давайте опишем с помощьюenum тип данных для хранения времени года:
enum Season { WINTER, SPRING, SUMMER, AUTUMN }
Ну и простой пример его использования:
Season season = Season.SPRING;
if (season == Season.SPRING) season = Season.SUMMER;
System.out.println(season);
В результате выполнения которого на консоль будет выведено SUMMER. Думаю, что пример очевиден и в пояснениях не нуждается.
Перечисление это класс
Объявляяenum мы неявно создаем класс производный от java.lang.Enum. Условно конструкция enum Season { ... } эквивалентна class Season extends java.lang.Enum { ... }. И хотя явным образом наследоваться от java.lang.Enum нам не позволяет компилятор, все же в том, что enum наследуется, легко убедиться с помощью reflection:
System.out.println(Season.class.getSuperclass());
На консоль будет выведено:
class java.lang.Enum
Собственно наследование за нас автоматически выполняет компилятор Java. Далее давайте условимся называть класс, созданный компилятором для реализации перечисления, enum-классом, а возможные значения перечисляемого типа элементами enum-a.
Элементы перечисления это экземпляры enum-класса, доступные статически
Элементыenum Season (WINTER, SPRING и т.д.) это статически доступные экземпляры enum-класса Season. Их статическая доступность позволяет нам выполнять сравнение с помощью оператора сравнения ссылок ==. Пример:
Season season = Season.SUMMER;
if (season == Season.AUTUMN) season = Season.WINTER;Название и порядковый номер элемента enum
Как уже было сказано ранее любойenum-класс наследует java.lang.Enum, который содержит ряд методов полезных для всех перечислений. Пример:
Season season = Season.WINTER;
System.out.println("season.name()=" + season.name() + " season.toString()=" + season.toString() + " season.ordinal()=" + season.ordinal());
Будет выведено:
season.name()=WINTER season.toString()=WINTER season.ordinal()=0
Здесь показано использования методов name(), toString() и ordinal(). Семантика методов очевидна. Следует обратить внимание, что данные методы enum-класс наследует из класса java.lang.Enum.
Получение элемента enum по строковому представлению его имени
Довольно часто возникает задача получить элементenum по его строковому представлению. Для этих целей в каждом enum-классе компилятор автоматически создает специальный статический метод: public static EnumClass valueOf(String name), который возвращает элемент перечисления EnumClass с названием, равным name. Пример использования:
String name = "WINTER";
Season season = Season.valueOf(name);
В результате выполнения кода переменная season будет равна Season.WINTER. Cледует обратить внимание, что если элемент не будет найден, то будет выброшен IllegalArgumentException, а в случае, если name равен null, то NullPointerException. Об этом, кстати, часто забывают. Почему-то многие твердо уверенны, что если функция принимает один аргумент и при некоторых условиях выбрасывает IllegalArgumentException, то при передачи туда null, также будет непременно выброшен IllegalArgumentException. Но это не относится к делу. Продолжим.
Получение всех элементов перечисления
Иногда необходимо получить список всех элементовenum-класса во время выполнения. Для этих целей в каждом enum-классе компилятор создает метод public static EnumClass[] values(). Пример использования:
System.out.println(Arrays.toString(Season.values()));
Получим вывод:
[WINTER, SPRING, SUMMER, AUTUMN]
Обратите внимание, что ни метод valueOf(), ни метод values() не определен в классе java.lang.Enum. Вместо этого они автоматически добавляются компилятором на этапе компиляции enum-класса. Обратите внимание и на то, что values() при каждом вызове возвращает новый массив: в декомпилированном коде ниже видно $VALUES.clone(). Менять полученный массив бесполезно, оригинал он не затронет, а в горячем цикле такой вызов лучше делать один раз и сохранять результат в переменную (или брать EnumSet.allOf(Type.class)).Методы перечисления: что откуда берется
Соберем разобранные методы в одну таблицу, чтобы было видно, что наследуется отjava.lang.Enum, а что дописывает компилятор:
| Метод | Что возвращает | Откуда берется |
|---|---|---|
name() |
имя элемента в точности так, как оно написано в коде | наследуется от java.lang.Enum |
toString() |
по умолчанию то же, что name(), но его можно переопределить |
наследуется от java.lang.Enum |
ordinal() |
порядковый номер элемента, считая с нуля | наследуется от java.lang.Enum |
compareTo(E) |
разницу порядковых номеров, поэтому элементы сортируются в порядке объявления | наследуется от java.lang.Enum |
equals() и hashCode() |
сравнение по ссылке: у каждого элемента ровно один экземпляр | наследуются от java.lang.Enum, объявлены final |
values() |
массив всех элементов в порядке объявления | добавляет компилятор |
valueOf(String) |
элемент с заданным именем или исключение | добавляет компилятор |
Добавляем свои методы в enum-класс
У Вас есть возможность добавлять собственные методы как вenum-класс, так и в его элементы:
enum Direction {
UP, DOWN;
public Direction opposite() { return this == UP ? DOWN : UP; }
}
То же, но с полиморфизмом:
enum Direction {
UP {
public Direction opposite() { return DOWN; }
},
DOWN {
public Direction opposite() { return UP; }
};
public abstract Direction opposite();
}
Последний пример демонстрирует использование наследования в enum. Об этом далее.
Наследование в enum
С помощьюenum в Java можно реализовать иерархию классов, объекты которой создаются в единственном экземпляре и доступны статически. При этом элементы enum могут содержать собственные конструкторы. Приведем пример:
enum Type {
INT(true) {
public Object parse(String string) { return Integer.valueOf(string); }
},
INTEGER(false) {
public Object parse(String string) { return Integer.valueOf(string); }
},
STRING(false) {
public Object parse(String string) { return string; }
};
boolean primitive;
Type(boolean primitive) { this.primitive = primitive; }
public boolean isPrimitive() { return primitive; }
public abstract Object parse(String string);
}
Здесь объявляется перечисление Type с тремя элементами INT, INTEGER и STRING. Компилятор создаст следующие классы и объекты:
Type: класс производный отjava.lang.EnumINT: объект 1-го класса производного отTypeINTEGER: объект 2-го класса производного отTypeSTRING: объект 3-го класса производного отType
Object parse(String) и конструктором Type(..., boolean). При этом объекты классов INT, INTEGER и STRING существуют в единственном экземпляре и доступны статически. В этом можно убедится:
System.out.println(Type.class);
System.out.println(Type.INT.getClass() + " " + Type.INT.getClass().getSuperclass());
System.out.println(Type.INTEGER.getClass() + " " + Type.INTEGER.getClass().getSuperclass());
System.out.println(Type.STRING.getClass() + " " + Type.STRING.getClass().getSuperclass());
Получим вывод:
class Type
class Type$1 class Type
class Type$2 class Type
class Type$3 class Type
Видно, что компилятор создал класс Type и 3 nested класса, производных от Type.
Наследоваться от обычного класса перечисление не может: место родителя уже занято классом java.lang.Enum. Зато enum может реализовывать интерфейсы, и это обычный способ встроить перечисление в чужую иерархию типов: интерфейс объявляет метод, а каждый элемент дает свою реализацию.
interface Parser {
Object parse(String string);
}
enum Type implements Parser {
INT(true) {
public Object parse(String string) { return Integer.valueOf(string); }
},
STRING(false) {
public Object parse(String string) { return string; }
};
private final boolean primitive;
Type(boolean primitive) { this.primitive = primitive; }
public boolean isPrimitive() { return primitive; }
}
Теперь Type можно передавать в любой код, который принимает Parser и ничего не знает о перечислениях.
Декомпилированный enum-class с наследованием
В подтверждение вышесказанному приведем еще результат декомпиляции перечисленияType из примера выше:
abstract class Type extends Enum {
public static Type[] values() {
return (Type[]) $VALUES.clone();
}
public static Type valueOf(String name) {
return (Type) Enum.valueOf(t / T$Type, name);
}
public boolean isPrimitive() {
return primitive;
}
public abstract Object parse(String s);
public static final Type INT;
public static final Type INTEGER;
public static final Type STRING;
boolean primitive;
private static final Type $VALUES[];
static {
INT = new Type("INT", 0, true) {
public Object parse(String string) { return Integer.valueOf(string); }
};
INTEGER = new Type("INTEGER", 1, false) {
public Object parse(String string) { return Integer.valueOf(string); }
};
STRING = new Type("STRING", 2, false) {
public Object parse(String string) { return string; }
};
$VALUES = (new Type[] {
INT, INTEGER, STRING
});
}
private Type(String s, int i, boolean primitive) {
super(s, i);
this.primitive = primitive;
}
}Перечисления и параметрический полиморфизм
У читателя может возникнуть вопрос: "почему вышеуказанное перечисление Type не использует генерики (generics)?". Дело в том, что в Java использование генериков вenum запрещено: в грамматике объявления перечисления просто нет места для параметров типа (JLS, раздел 8.9). Так следующий пример не скомпилируется:
enum Type<T> {}Дальнейшее изучение
Для более глубокого понимания того, как работают перечисления в Java рекомендую ознакомиться с исходными кодами классаjava.lang.Enum, а также посмотреть на сгенерированный код. Декомпилятор Jad, которым сделан пример выше, остановился в развитии в 2001 году и с современными классами не справляется, поэтому берите то, что есть под рукой: команду javap -c -p Season.class из JDK или встроенный декомпилятор IntelliJ IDEA (он открывает .class по двойному щелчку). Более того, изучение исходных кодов библиотеки Java абсолютно необходимо для понимания принципов работы многих механизмов в Java и полезно как эталон объектно-ориентированного дизайна.
Вопросы и ответы
Что такое enum в Java и зачем он нужен?
enum это тип данных с заранее заданным набором значений: enum Season { WINTER, SPRING, SUMMER, AUTUMN }. Он нужен там, где значений немного и они известны заранее: времена года, дни недели, статусы заказа, роли пользователя. По сравнению с константами int или String перечисление дает проверку типов на этапе компиляции: в переменную типа Season нельзя случайно положить число 15 или строку с опечаткой.
Чем enum отличается от обычного класса?
Компилятор сам делает такой класс наследникомjava.lang.Enum, поэтому наследоваться от другого класса enum уже не может (интерфейсы реализовывать может). Экземпляры создает тоже компилятор, по одному на каждый элемент, и создать новый через new нельзя. Обобщенным перечисление быть не может: enum Type<T> { } это ошибка компиляции (JLS, раздел 8.9).
Как получить элемент enum по строке и что будет при ошибке?
МетодомvalueOf(String): Season.valueOf("WINTER") вернет Season.WINTER. Имя должно совпадать точно, с учетом регистра. Если такого элемента нет, метод бросит IllegalArgumentException, а если передать null, то NullPointerException. Поэтому строки из внешних источников обычно оборачивают в try-catch или сравнивают вручную в цикле по values().
Чем name() отличается от toString() и ordinal()?
name() возвращает имя элемента ровно так, как оно написано в коде, и переопределить его нельзя: метод объявлен final. toString() по умолчанию возвращает то же самое, но его разрешено переопределить, чтобы показывать пользователю «Зима» вместо WINTER. ordinal() дает порядковый номер элемента, считая с нуля. На ordinal() лучше не завязывать логику и тем более не хранить его в базе: стоит вставить новый элемент в середину списка, и все номера сдвинутся.
Можно ли использовать enum в switch?
Да, и это самый частый сценарий. В меткахcase имя класса не пишут: case WINTER:, а не case Season.WINTER:. С Java 14 работает и стрелочная форма case WINTER -> ..., где не нужен break, а switch-выражение по перечислению может обойтись без ветки default, если перечислены все элементы. Подробнее про обе формы в статье про оператор switch.
Что такое EnumSet и EnumMap?
Это специальные коллекции изjava.util, рассчитанные на перечисления. EnumSet хранит набор элементов enum в виде битовой маски, а EnumMap использует ordinal() как индекс массива. Обе работают быстрее и занимают меньше памяти, чем HashSet и HashMap, и обходят элементы в порядке объявления. Если ключами служат элементы перечисления, брать стоит именно их: подробнее про коллекции в обзоре Java Collections Framework.
Читайте также: