Предисловие
В этой статье я хочу разобраться в теме перечислений и поделиться результатами своих исследований.
Основой моего анализа станет спецификация Java, а именно раздел, касающиеся ENUM
(Java Language Specification SE 24). Я постараюсь превратить сложные формулировки в понятное и доступное изложение с кучей примеров. Цель - сделать статью интересной и легко читаемой, убрав лишнюю техническую сложность.
Что такое ENUM?
Я не вижу смысла придумывать собственное определение, ведь разработчики Java уже дали точную формулировку в Java Tutorial:
Тип Enum — это специальный тип данных, который позволяет переменной быть набором предопределенных констант. Переменная должна быть равна одному из значений, которые были для нее предопределены.
Следуя из определения, можно сделать вывод, что enum очень удобен, когда нужно работать с фиксированным набором значений. Например, представим, что у нас есть система заказов, где статус заказа может быть "NEW", "PROCESSING", "COMPLETED". И вместо обычных строковых значений, или других дизайнерских решений, которые чреваты ошибками (например, "new" вместо "NEW"), ENUM гарантирует, что состояние объекта всегда будет принимать только заранее определенные значения. Таким образом, ENUM здорово упрощает работу с фиксированным набором значений, чётко определяя возможные состояния объектов, делая код более структурированным, удобочитаемым и защищенным от случайных ошибок.
public enum OrderStatus {
NEW, PROCESSING, COMPLETED;
public boolean isFinal() {
return this == COMPLETED;
}
}
public class Order {
private OrderStatus status;
public Order() {
this.status = OrderStatus.NEW;
}
public void updateStatus(OrderStatus newStatus) {
if (status.isFinal()) {
System.out.println("Невозможно изменить статус: заказ уже завершён.");
} else {
this.status = newStatus;
System.out.println("Статус заказа обновлён: " + status);
}
}
public void printStatus() {
System.out.println("Текущий статус заказа: " + status);
}
public static void main(String[] args) {
Order order = new Order(); // Создание нового заказа
order.printStatus();
order.updateStatus(OrderStatus.PROCESSING); // Обновление статуса
order.updateStatus(OrderStatus.COMPLETED); // Завершение заказа
order.updateStatus(OrderStatus.NEW); // Попытка изменить уже завершённый заказ
}
}
Вывод:
Текущий статус заказа: NEW
Статус заказа обновлён: PROCESSING
Статус заказа обновлён: COMPLETED
Невозможно изменить статус: заказ уже завершён.
Прочтение спецификации
Теперь, когда мы понимаем, что такое enum и умеем его использовать, самое время обратиться к официальной спецификации, чтобы изучить все детали работы enum.
8.9. Enum Classes
Когда мы объявляем enum, мы создаем новый тип данных, который является специальным классом. В отличие от обычных классов, у enum есть фиксированный набор экземпляров (констант), которые задаются непосредственно в его теле.
Синтаксис: {ClassModifier} enum TypeIdentifier [ClassImplements] EnumBody
ClassModifier (Опционально): Аннотации,
public
,protected
,private
,static
,strictfp
. Если объявление enum содержит модификаторabstract
,final
,sealed
илиnon-sealed
, это приведет к ошибке компиляции.TypeIdentifier: Имя enum-класса, из нашего первого примера это
OrderStatus
.ClassImplements (Опционально): enum, как и обычные классы, могут имплементировать интерфейсы. Это имя используется для ссылки на этот enum.
EnumBody: Тело перечисления, в котором перечисляются все доступные экземпляры и, если необходимо, дополнительные поля, методы или конструкторы.
Enum может быть классом верхнего уровня, вложенными или локальный:
-
Enum-класс верхнего уровня:
public enum Color { RED, GREEN, BLUE }
-
Вложенный enum - определяется внутри другого класса:
public class Car { public enum Status { NEW, USED, SOLD } public static void main(String[] args) { Status carStatus = Status.NEW; System.out.println(carStatus); } }
-
Локальный enum - внутри метода или другого блока кода:
public class Example { public void printDay() { enum Day { MONDAY, TUESDAY, WEDNESDAY } Day today = Day.MONDAY; System.out.println("Сегодня: " + today); } public static void main(String[] args) { new Example().printDay(); } }
Класс enum либо неявно final
, либо неявно sealed
:
-
неявно
final
, если его объявление не содержит констант с телом класса:public enum Color { RED, GREEN, BLUE }
Ни одна из констант (
RED
,GREEN
,BLUE
) не имеет собственного тела, поэтому классColor
является не явноfinal
. Т.е. невозможно создать подклассы дляColor
. -
неявно
sealed
, если хотя бы у одной константы есть тело класса.public enum CalculatorOperation { // Константа с переопределяемым методом ADD { @Override public double apply(double a, double b) { System.out.println(getDescription()); return a + b; } // Дополнительный метод, специфичный для этой константы public String getDescription() { return "Осуществляет сложение"; } }, // Константа, которая переопределяет метод, но без дополнительных методов SUBTRACT { @Override public double apply(double a, double b) { return a - b; } }, // Константы, которые НЕ переопределяют метод – используют реализацию по умолчанию MULTIPLY, DIVIDE; // Базовый метод apply, используемый константами, не имеющими собственного переопределения public double apply(double a, double b) { throw new UnsupportedOperationException("Операция не определена для: " + this); } }
Пример использования CalculatorOperation:
public static void main(String[] args) { double x = 10; double y = 5; System.out.println("ADD:"); double resultAdd = CalculatorOperation.ADD.apply(x, y); System.out.println(" Результат: " + resultAdd); System.out.println("SUBTRACT:"); double resultSubtract = CalculatorOperation.SUBTRACT.apply(x, y); System.out.println(" Результат: " + resultSubtract); // Использование константы MULTIPLY, которая не имеет собственного тела. System.out.println("MULTIPLY:"); try { double resultMultiply = CalculatorOperation.MULTIPLY.apply(x, y); System.out.println(" Результат: " + resultMultiply); } catch (UnsupportedOperationException ex) { System.out.println(" Результат: " + ex.getMessage()); } }
Вывод:
ADD: Осуществляет сложение Результат: 15.0 SUBTRACT: Результат: 5.0 MULTIPLY: Результат: Операция не определена для: MULTIPLY
В этом примере
CalculatorOperation
содержит четыре константы:ADD
- имеет собственное тело, в котором переопределен методapply
и вызывается собственный методgetDescription
.SUBTRACT
c переопределенным методомapply
.MULTIPLY
иDIVIDE
для которой не задано собственного тела, и вызовapply
приводит к выбросу исключения (логика базового метода).
В данном случае, т.к.ADD
иSUBTRACT
имеют тело, тоCalculatorOperation
является неявноsealed
, и его прямыми подклассами являются анонимные классы.MULTIPLY
иDIVIDE
не имеют тела, поэтому они остаются экземпляром базового типаCalculatorOperation
:public static void main(String[] args) { System.out.println("Класс ADD: " + CalculatorOperation.ADD.getClass().getName()); System.out.println("Класс SUBTRACT: " + CalculatorOperation.SUBTRACT.getClass().getName()); System.out.println("Класс MULTIPLY: " + CalculatorOperation.MULTIPLY.getClass().getName()); System.out.println("Класс DIVIDE: " + CalculatorOperation.DIVIDE.getClass().getName()); // ADD и SUBTRACT анонимные подклассы CalculatorOperation System.out.println(CalculatorOperation.ADD.getClass() == CalculatorOperation.SUBTRACT.getClass()); // Оба элемента являются экземплярами одного класса System.out.println(CalculatorOperation.MULTIPLY.getClass() == CalculatorOperation.DIVIDE.getClass()); }
Вывод:
Класс ADD: com.example.CalculatorOperation$1 Класс SUBTRACT: com.example.CalculatorOperation$2 Класс MULTIPLY: com.example.CalculatorOperation Класс DIVIDE: com.example.CalculatorOperation false true
Каждый локальный или вложенный enum является статическим. Для объявления вложенного enum допускается избыточное указание модификатора static
, но для локального это не разрешается:
public class EnumExample {
// Вложенный enum-класс без явного модификатора static (но он всё равно статический)
enum Color {
RED, GREEN, BLUE
}
// Вложенный enum-класс с явным указанием модификатора static (избыточно, но допускается)
static enum Direction {
NORTH, SOUTH, EAST, WEST
}
public void demonstrateLocalEnum() {
// Локальный enum-класс внутри метода. Модификатор static здесь указать нельзя.
enum Size {
SMALL, MEDIUM, LARGE
}
}
}
При объявлении enum нельзя указывать один и тот же модификатор доступа более одного раза или нескольких модификаторов доступа, иначе это приведет к ошибке компиляции.
Каждый enum-класс неявно наследуется от класса java.lang.Enum<E>
, где E
это конкретный тип нашего перечисления. Когда мы создаем enum, генерируется класс, который расширяет абстрактный Enum<E>
. Этот базовый класс предоставляет общую функциональность для всех enum, предоставляя такие методы, как name()
, ordinal()
и другие. Сам класс Enum<E>
планирую подробно рассмотреть в одной из следующей статье данной серии.
Для enum нет секции extends
и нельзя явно указать тип прямого суперкласса, даже Enum<E>
. Компилятор автоматически наследует enum от Enum<E>
.
Enum не имеет экземпляров, за исключением тех, которые определены его константами. Попытка явно создать экземпляр enum с помощью оператора new
приведет к ошибке компиляции:
В примере ниже enum Color
имеет ровно три экземпляра: Color.RED,
Color.GREEN
, Color.BLUE
, они единственные объекты типа Color
. Их нельзя дополнительно создать (Color color = new Color();
).
public enum Color {
RED, GREEN, BLUE
}
Помимо ошибки на этапе компиляции, есть еще 3 механизма, гарантирующие, что экземпляры enum класса не существуют кроме тех, которые определены его константами:
-
Финальный метод
clone
в супер классе Enum, запрещающий его переопределение. К тому же он еще иprotected
, что означает, чтоclone
доступен только для классов внутри пакета, в котором определен Enum или из классов, наследующих Enum. Но поскольку enum не может наследоваться (т.к. он либо финальный, либо не допускает пользовательского наследования), то доступ к методуclone
ограничен механизмами Java:protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
Рефлексивное создание экземпляров enum запрещено.
-
При сериализации и десериализации объекта enum механизм сериализации обрабатывает enum так, чтобы всегда возвращался уже существующий экземпляр:
public static void main(String[] args) { Season original = Season.SUMMER; Season deserialized = null; // Сериализация объекта Season.SUMMER в файл try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("season.ser"))) { oos.writeObject(original); } catch (IOException e) { e.printStackTrace(); } // Десериализация объекта из файла try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("season.ser"))) { deserialized = (Season) ois.readObject(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } // Проверка: должны быть одним и тем же экземпляром System.out.println("Исходная константа: " + original); // Вывод: SUMMER System.out.println("Десериализованная константа: " + deserialized); // Вывод: SUMMER System.out.println("original == deserialized: " + (original == deserialized)); // Вывод: true }
8.9.1. Enum Constants
Тело объявления enum может содержать константы enum. Константа enum определяет экземпляр enum-класса.
Синтаксис:
EnumBody:
{ [EnumConstantList] [,] [EnumBodyDeclarations] }
EnumConstantList:
EnumConstant {, EnumConstant}
EnumConstant:
{EnumConstantModifier} Identifier [( [ArgumentList] )] [ClassBody]
EnumConstantModifier:
Annotation
тело объявления enum задается в фигурных скобках.
-
в теле enum может присутствовать:
EnumConstantList: список констант перечисления (если он есть, то он указывается первым). Список констант может состоять как минимум из одной константы. Несколько констант разделяются запятыми.
не обязательная запятая, которая может стоять после списка констант.
EnumBodyDeclarations - дополнительные объявления, такие как поля, методы или вложенные классы, которые следуют после констант.
EnumConstant: элемент списка констант. Перед константой не обязательный модификатор (разрешены только аннотации, другие модификаторы, такие как public, static и т.д. использовать нельзя). Имя константы, причем в отличие от имени enum-класса допускаются указывать такие ключевые слова как:
permits
,record
,sealed
,var
иyield
. Необязательный список аргументов в круглых скобках после имени константы, которые передаются конструктору num-класса. И необязательное тело класса в фигурных скобках, если необходимо константе добавить дополнительное поведение.
Пример, демонстрирующий все возможности, описанные выше:
public enum FullyEnumExample {
// Константа с аннотацией, аргументами и собственным телом (переопределение метода describe)
@Deprecated
CONSTANT_ONE("One", 1) {
@Override
public void describe() {
System.out.println("I am CONSTANT_ONE with label: " + getLabel() + " and value: " + getValue());
}
},
// Константа с одним аргументом
CONSTANT_TWO("Two"),
// Константа без собственного тела, с двумя аргументами
CONSTANT_THREE("Three", 3),
// Константа без аргументов
CONSTANT_FOUR, // допускается завершающая запятая
;
private final String label;
private final int value;
FullyEnumExample(String label, int value) {
this.label = label;
this.value = value;
}
FullyEnumExample(String label) {
this(label, 0);
}
FullyEnumExample() {
this("Default", 0);
}
public String getLabel() {
return label;
}
public int getValue() {
return value;
}
public void describe() {
System.out.println("Enum constant " + this + ": label = " + label + ", value = " + value);
}
public static void printAllConstants() {
for (FullyEnumExample constant : FullyEnumExample.values()) {
constant.describe();
}
}
static {
System.out.println("FullyEnumExample is being initialized.");
}
}
Имя задаваемое константе, автоматически становится полем enum-класса, через которое можно получить доступ к константе:Color.RED
.
Для константы enum можно указать аргументы, которые будут переданы в конструктор во время ee создания. Причем выбирается подходящий конструктор по стандартным правилам перегрузки. Если никаких аргументов не указано, то вызовется конструктор без параметров.
Если для константы enum указано собственное тело, то для этой константы автоматически создается анонимный класс. Этот класс является прямым потомком enum-класса и помечен как final. Он подчиняется тем же правилам, что и обычные анонимные классы, например, в нем нельзя объявлять конструкторы. Методы, объявленные в этом анонимном классе, могут использоваться за пределами enum только если они переопределяют уже существующие и доступные методы в основном enum-классе.
Нельзя определить абстрактный метод внутри тела константы enum, если это сделать, будет ошибка компиляции.
public enum ExampleEnum {
VALUE {
// Попытка объявить абстрактный метод вызовет ошибку компиляции.
public abstract void display();
}
}
Поскольку для каждой константы enum существует только один экземпляр, разрешается использовать оператор ==
вместо метода equals
при сравнении двух ссылок на объект, если известно, что хотя бы одна из них ссылается на константу enum.
Метод equals
в классе Enum
является final
и просто вызывает super.equals
, возвращая результат сравнения по идентичности.
Но на самом деле метод equals
в Enum
выглядит так:
public final boolean equals(Object other) {
return this == other;
}
Т.е. он напрямую возвращает результат проверки, указывают ли две ссылки на один и тот же объект.
Заключение
Чтобы не делать статью огромной и дать читателям глубже погрузиться в тонкости работы с enum, я планирую разделить прочтение спецификации на пару частей, а так же посвятить отдельное внимание классу Enum.
А пока что, мы изучили некоторые особенности enum в Java и убедились, что это не просто синтаксический сахар, а полноценный механизм, обеспечивающий безопасность типов и неизменяемость значений.
SimSonic
Эх, а я по заголовку подумал, что речь про кодогенерацию из OpenAPI спеки :)
BurnoutMid Автор
В следующий раз D