Предисловие

В этой статье я хочу разобраться в теме перечислений и поделиться результатами своих исследований.

Основой моего анализа станет спецификация 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 может быть классом верхнего уровня, вложенными или локальный:

  1. Enum-класс верхнего уровня:

    public enum Color {
      RED, GREEN, BLUE
    }
    
  2. Вложенный enum - определяется внутри другого класса:

     public class Car {
         public enum Status {
             NEW, USED, SOLD
         }
    
         public static void main(String[] args) {
             Status carStatus = Status.NEW;
             System.out.println(carStatus);
         }
     }
    
  3. Локальный 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 и убедились, что это не просто синтаксический сахар, а полноценный механизм, обеспечивающий безопасность типов и неизменяемость значений.

Комментарии (3)


  1. SimSonic
    13.06.2025 02:57

    Эх, а я по заголовку подумал, что речь про кодогенерацию из OpenAPI спеки :)


    1. BurnoutMid Автор
      13.06.2025 02:57

      В следующий раз D


  1. keekkenen
    13.06.2025 02:57

    хорошая статья, но возникает вопрос:

    - а, кто ? кто директор-то?