Перечисление (
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> не скомпилируется), зато может реализовывать интерфейсы.
Программируя мы часто сталкиваемся с необходимостью ограничить множество допустимых значений для некоторого типа данных. Так, например, день недели может иметь 7 разных значений, месяц в году 12, а время года 4. Для решения подобных задач во многих языках программирования со статической типизацией предусмотрен специальный тип данных: перечисление (
enum). В Java перечисление появилось не сразу. Специализированная языковая конструкция
enum была введена начиная с версии 1.5. До этого момента программисты использовали другие методы для реализации перечислений.
![Семь осенних листьев на веревке: заглавная картинка статьи о перечислениях в Java]()
Конструкция 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.Enum
INT: объект 1-го класса производного от Type
INTEGER: объект 2-го класса производного от Type
STRING: объект 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.
Читайте также:
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ