$ sudo teach IT
МОДУЛЬ 9 · УРОК 1

enum — Перечисления в Java

Знакомимся с перечислениями (enum): константы, поля, методы, конструкторы, switch, EnumSet и EnumMap — мощные инструменты для типизированных наборов значений

~30 минут Для новичков Java

Зачем нужны перечисления?

Представьте, что вы разрабатываете программу для управления светофором. У светофора есть три состояния: красный, жёлтый и зелёный. Как бы вы описали эти состояния в Java?

Первое, что приходит в голову — использовать константы:

public class LightColor {
    public static final int RED = 0;
    public static final int YELLOW = 1;
    public static final int GREEN = 2;
}

Это работает, но у такого подхода есть серьёзные проблемы:

  • Константы — это просто числа. Программист может передать любое число, даже 42, и компилятор не ругается
  • Нет удобного способа перебрать все возможные значения
  • Код выглядит нечитаемо: if (color == 0) — что такое 0?

Перечисления (enum) решают все эти проблемы. По сути, enum — это специальный тип класса, который представляет собой фиксированный набор именованных констант. Это как список вариантов, из которого нельзя выйти за рамки.

Простое перечисление

Создадим перечисление для цветов светофора:

public enum LightColor {
    RED,
    YELLOW,
    GREEN
}

Обратите внимание на несколько важных моментов:

  • Используется ключевое слово enum вместо class
  • Константы перечисляются через запятую
  • Последняя константа не обязана заканчиваться точкой с запятой (но можно)
  • Имена констант пишутся ЗАГЛАВНЫМИ БУКВАМИ по conventions

Теперь используем это перечисление:

public class TrafficLight {
    public static void main(String[] args) {
        LightColor current = LightColor.RED;
        System.out.println(current);

        if (current == LightColor.RED) {
            System.out.println("Стой!");
        }

        LightColor next = LightColor.GREEN;
        System.out.println(next);
    }
}

Вывод программы:

RED
Стой!
GREEN

Сравните: вместо загадочного if (color == 0) мы пишем понятное if (current == LightColor.RED). Код стал самодокументируемым!

Аналогия: Перечисление — это как меню ресторана. Вы можете выбрать только то, что в списке: «Суп», «Салат», «Десерт». Вы не можете заказать «42» или «Ракету» — таких вариантов в меню нет.

Методы enum

Каждое перечисление в Java автоматически имеет несколько полезных методов. Давайте разберём их на примере:

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class DayDemo {
    public static void main(String[] args) {
        Day day = Day.FRIDAY;

        System.out.println("name(): " + day.name());
        System.out.println("ordinal(): " + day.ordinal());
        System.out.println("toString(): " + day.toString());
        System.out.println("equals(): " + day.equals(Day.FRIDAY));

        System.out.println("valueOf(): " + Day.valueOf("MONDAY"));

        System.out.println("values(): ");
        for (Day d : Day.values()) {
            System.out.println("  " + d);
        }
    }
}

Вывод программы:

name(): FRIDAY
ordinal(): 4
toString(): FRIDAY
equals(): true
valueOf(): MONDAY
values():
  MONDAY
  TUESDAY
  WEDNESDAY
  THURSDAY
  FRIDAY
  SATURDAY
  SUNDAY

Давайте разберём каждый метод подробнее:

name() — имя константы

Возвращает имя константы как строку. Например, Day.FRIDAY.name() вернёт "FRIDAY". Это то же самое, что вызвать toString(), но name() нельзя переопределить, а toString() — можно.

ordinal() — порядковый номер

Возвращает порядковый номер константы, начиная с 0. Day.MONDAY.ordinal() вернёт 0, Day.TUESDAY.ordinal() вернёт 1 и так далее.

Важно: Не используйте ordinal() для сравнения значений! Если вы вставите новую константу в середину перечисления, все номера сдвинутся. Используйте сравнение через ==.

valueOf(String) — строку в константу

Преобразует строку в константу перечисления. Строка должна точно совпадать с именем константы (регистр важен!). Если константы с таким именем нет — будет выброшено исключение IllegalArgumentException.

Day monday = Day.valueOf("MONDAY");   // работает
Day error = Day.valueOf("monday");    // IllegalArgumentException!
Day error2 = Day.valueOf("HOLIDAY");  // IllegalArgumentException!

values() — все константы

Возвращает массив всех констант перечисления в порядке их объявления. Это позволяет легко перебрать все значения:

Day[] allDays = Day.values();
for (Day d : allDays) {
    System.out.println(d.name() + " — порядковый номер: " + d.ordinal());
}

Поля и методы в enum

Перечисления — это полноценные классы, поэтому они могут иметь поля, конструкторы и методы. Давайте создадим более сложный пример — перечисление планет с характеристиками:

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS(4.869e+24, 6.0518e6),
    EARTH(5.976e+24, 6.37814e6),
    MARS(6.421e+23, 3.3972e6),
    JUPITER(1.9e+27, 7.1492e7),
    SATURN(5.688e+26, 6.0268e7),
    URANUS(8.686e+25, 2.5559e7),
    NEPTUNE(1.024e+26, 2.4746e7);

    private final double mass;
    private final double radius;

    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    public double getMass() {
        return mass;
    }

    public double getRadius() {
        return radius;
    }

    static final double G = 6.67300E-11;

    public double surfaceGravity() {
        return G * mass / (radius * radius);
    }

    public double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
}

Здесь каждая планета хранит свою массу и радиус. Конструктор перечисления вызывается для каждой константы и передаёт соответствующие значения. Давайте используем:

public class PlanetDemo {
    public static void main(String[] args) {
        double earthWeight = 75.0;
        double earthMass = earthWeight / Planet.EARTH.surfaceGravity();

        System.out.println("Вес на Земле: " + earthWeight + " кг");
        System.out.println();

        for (Planet p : Planet.values()) {
            System.out.printf("Вес на %s: %.2f кг%n",
                p, p.surfaceWeight(earthMass));
        }
    }
}

Вывод программы:

Вес на Земле: 75.0 кг

Вес на MERCURY: 28.34 кг
Вес на VENUS: 68.10 кг
Вес на EARTH: 75.00 кг
Вес на MARS: 28.25 кг
Вес на JUPITER: 177.73 кг
Вес на SATURN: 80.35 кг
Вес на URANUS: 67.53 кг
Вес на NEPTUNE: 84.45 кг

Мы видим, что каждая планета «знает» свои характеристики и может рассчитать вес объекта на своей поверхности. Это мощная возможность — данные и поведение связаны в одном месте.

Аналогия: Перечисление с полями — это как набор визитных карточек. Каждая карточка (константа) содержит имя, телефон и адрес (поля). Вы можете перебрать все карточки и узнать нужную информацию у каждой.

Конструктор в enum

Конструктор перечисления имеет особенности:

  • Конструктор всегда private (явно или неявно). Нельзя создать экземпляр перечисления извне
  • Конструктор вызывается для каждой константы в том порядке, как они объявлены
  • Если конструктор не объявлен, перечисление использует конструктор без параметров
public enum Season {
    WINTER("Зима", "Холодно"),
    SPRING("Весна", "Теплеет"),
    SUMMER("Лето", "Жарко"),
    AUTUMN("Осень", "Холодеет");

    private final String name;
    private final String description;

    Season(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public String getInfo() {
        return name + " — " + description;
    }
}

Попробуйте вызвать конструктор перечисления извне:

Season s = new Season("Лето", "Жарко");  // ОШИБКА КОМПИЛЯЦИИ!

Комилятор не позволит этого сделать, потому что конструктор перечисления автоматически становится private.

Абстрактные методы в enum

Каждая константа перечисления может реализовывать абстрактный метод по-своему. Это полезно, когда поведение констант различается:

public enum Operation {
    ADD {
        public double apply(double x, double y) {
            return x + y;
        }
    },
    SUBTRACT {
        public double apply(double x, double y) {
            return x - y;
        }
    },
    MULTIPLY {
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        public double apply(double x, double y) {
            if (y == 0) throw new ArithmeticException("Деление на 0");
            return x / y;
        }
    };

    public abstract double apply(double x, double y);
}

public class OperationDemo {
    public static void main(String[] args) {
        double a = 10.0;
        double b = 3.0;

        for (Operation op : Operation.values()) {
            System.out.printf("%.1f %s %.1f = %.2f%n",
                a, op, b, op.apply(a, b));
        }
    }
}

Вывод программы:

10.0 ADD 3.0 = 13.00
10.0 SUBTRACT 3.0 = 7.00
10.0 MULTIPLY 3.0 = 30.00
10.0 DIVIDE 3.0 = 3.33

Здесь каждая операция реализует метод apply() по-своему. Это пример полиморфизма в перечислениях!

Переопределение toString()

По умолчанию метод toString() возвращает имя константы (как и name()). Но его можно переопределить для красивого вывода:

public enum Month {
    JANUARY("Январь"),
    FEBRUARY("Февраль"),
    MARCH("Март"),
    APRIL("Апрель"),
    MAY("Май"),
    JUNE("Июнь"),
    JULY("Июль"),
    AUGUST("Август"),
    SEPTEMBER("Сентябрь"),
    OCTOBER("Октябрь"),
    NOVEMBER("Ноябрь"),
    DECEMBER("Декабрь");

    private final String russianName;

    Month(String russianName) {
        this.russianName = russianName;
    }

    @Override
    public String toString() {
        return russianName;
    }

    public String getRussianName() {
        return russianName;
    }
}

public class MonthDemo {
    public static void main(String[] args) {
        System.out.println(Month.JANUARY);          // Январь
        System.out.println(Month.JANUARY.name());    // JANUARY
        System.out.println(Month.JANUARY.toString()); // Январь
    }
}

Как видите, name() и toString() возвращают разные значения. Метод name() всегда возвращает原始ное имя, а toString() может быть переопределён.

Перечисления в switch

Перечисления идеально подходят для конструкции switch, потому что перечисляют все возможные варианты. При этом в кейсах можно писать просто имя константы без указания типа перечисления:

public enum TrafficLight {
    RED, YELLOW, GREEN
}

public class TrafficLightDemo {
    public static void main(String[] args) {
        TrafficLight light = TrafficLight.YELLOW;

        switch (light) {
            case RED:
                System.out.println("Стой! Красный свет.");
                break;
            case YELLOW:
                System.out.println("Жди! Жёлтый свет.");
                break;
            case GREEN:
                System.out.println("Иди! Зелёный свет.");
                break;
        }
    }
}

Вывод программы:

Жди! Жёлтый свет.

Совет: Если в switch по перечислению не хватает case для какого-то значения, компилятор выдаст предупреждение. Это очень удобно — вы не забудете обработать новый вариант!

EnumSet —高效集合

EnumSet — это специализированная коллекция для перечислений. Она работает намного быстрее HashSet, потому что внутри использует битовые маски.

import java.util.EnumSet;
import java.util.Set;

public enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

public class EnumSetDemo {
    public static void main(String[] args) {
        Set<Day> weekdays = EnumSet.range(Day.MONDAY, Day.FRIDAY);
        System.out.println("Будни: " + weekdays);

        Set<Day> weekend = EnumSet.of(Day.SATURDAY, Day.SUNDAY);
        System.out.println("Выходные: " + weekend);

        Set<Day> allDays = EnumSet.allOf(Day.class);
        System.out.println("Все дни: " + allDays);

        Set<Day> notWeekend = EnumSet.complementOf(weekend);
        System.out.println("Не выходные: " + notWeekend);

        Set<Day> empty = EnumSet.noneOf(Day.class);
        System.out.println("Пустой: " + empty);
    }
}

Вывод программы:

Будни: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
Выходные: [SATURDAY, SUNDAY]
Все дни: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
Не выходные: [MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]
Пустой: []

Основные фабричные методы EnumSet:

  • noneOf(Class<E>) — пустой набор
  • allOf(Class<E>) — все константы
  • of(E...) — указанные константы
  • range(E, E) — диапазон от первой до второй (включительно)
  • complementOf(Set<E>) — все, кроме указанных

EnumMap —高效映射

EnumMap — это специализированная Map, где ключом является перечисление. Она намного быстрее обычного HashMap.

import java.util.EnumMap;
import java.util.Map;

public enum Season {
    WINTER, SPRING, SUMMER, AUTUMN
}

public class EnumMapDemo {
    public static void main(String[] args) {
        Map<Season, String> activities = new EnumMap<>(Season.class);
        activities.put(Season.WINTER, "Кататься на лыжах");
        activities.put(Season.SPRING, "Пробуждение природы");
        activities.put(Season.SUMMER, "Купаться в море");
        activities.put(Season.AUTUMN, "Собирать урожай");

        for (Season season : Season.values()) {
            System.out.println(season + ": " + activities.get(season));
        }
    }
}

Вывод программы:

WINTER: Кататься на лыжах
SPRING: Пробуждение природы
SUMMER: Купаться в море
AUTUMN: Собирать урожай

Преимущества EnumMap перед HashMap:

  • Быстрее — нет вычисления хеш-кода
  • Экономит память — нет хранения хешей
  • Порядок ключей соответствует порядку объявления в enum
  • Все ключи гарантированно принадлежат одному типу

Практический пример: состояние заказа

Давайте создадим перечисление для состояния заказа в интернет-магазине с методами, проверяющими допустимые переходы:

import java.util.EnumSet;
import java.util.Set;

public enum OrderStatus {
    CREATED("Создан"),
    CONFIRMED("Подтверждён"),
    SHIPPED("Отправлен"),
    DELIVERED("Доставлен"),
    CANCELLED("Отменён");

    private final String description;

    OrderStatus(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }

    public boolean canTransitionTo(OrderStatus next) {
        switch (this) {
            case CREATED:
                return next == CONFIRMED || next == CANCELLED;
            case CONFIRMED:
                return next == SHIPPED || next == CANCELLED;
            case SHIPPED:
                return next == DELIVERED;
            case DELIVERED:
            case CANCELLED:
                return false;
            default:
                return false;
        }
    }

    public OrderStatus transitionTo(OrderStatus next) {
        if (canTransitionTo(next)) {
            System.out.println("Переход: " + this + " → " + next);
            return next;
        } else {
            System.out.println("Нельзя перейти из " + this + " в " + next);
            return this;
        }
    }

    public Set<OrderStatus> getPossibleNextStatuses() {
        Set<OrderStatus> possible = EnumSet.noneOf(OrderStatus.class);
        for (OrderStatus status : OrderStatus.values()) {
            if (canTransitionTo(status)) {
                possible.add(status);
            }
        }
        return possible;
    }
}
public class OrderDemo {
    public static void main(String[] args) {
        OrderStatus status = OrderStatus.CREATED;
        System.out.println("Текущий статус: " + status);
        System.out.println("Возможные следующие: " + status.getPossibleNextStatuses());

        status = status.transitionTo(OrderStatus.CONFIRMED);
        System.out.println("Текущий статус: " + status);

        status = status.transitionTo(OrderStatus.SHIPPED);
        System.out.println("Текущий статус: " + status);

        status = status.transitionTo(OrderStatus.CANCELLED);
        System.out.println("Текущий статус: " + status);
    }
}

Вывод программы:

Текущий статус: CREATED
Возможные следующие: [CONFIRMED, CANCELLED]
Переход: CREATED → CONFIRMED
Текущий статус: CONFIRMED
Переход: CONFIRMED → SHIPPED
Текущий статус: SHIPPED
Нельзя перейти из SHIPPED в CANCELLED
Текущий статус: SHIPPED

Зачем это нужно: Перечисления идеально подходят для моделирования конечных автоматов — систем, где объект может находиться в одном из фиксированных состояний и переходить между ними по определённым правилам. Это типичный паттерн для заказов, задач, жизненного цикла объектов.

Enum и интерфейсы

Перечисления могут реализовывать интерфейсы. Это позволяет задать общий контракт для всех констант:

public interface Describable {
    String getDescription();
}

public enum Animal implements Describable {
    DOG("Собака", "Верный друг человека"),
    CAT("Кошка", "Грациозное создание"),
    BIRD("Птица", "Парит в небе");

    private final String name;
    private final String description;

    Animal(String name, String description) {
        this.name = name;
        this.description = description;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public String getName() {
        return name;
    }
}

public class DescribableDemo {
    public static void printDescription(Describable d) {
        System.out.println(d.getDescription());
    }

    public static void main(String[] args) {
        printDescription(Animal.DOG);
        printDescription(Animal.CAT);
    }
}

Псевдополя перечисления

Перечисление может иметь специальные методы:

public enum Planet {
    MERCURY, VENUS, EARTH, MARS,
    JUPITER, SATURN, URANUS, NEPTUNE;

    public Planet next() {
        Planet[] values = values();
        return values[(ordinal() + 1) % values.length];
    }

    public Planet previous() {
        Planet[] values = values();
        return values[(ordinal() - 1 + values.length) % values.length];
    }
}

public class PlanetNavigation {
    public static void main(String[] args) {
        Planet earth = Planet.EARTH;
        System.out.println("Текущая: " + earth);
        System.out.println("Следующая: " + earth.next());
        System.out.println("Предыдущая: " + earth.previous());
    }
}

Вывод программы:

Текущая: EARTH
Следующая: MARS
Предыдущая: VENUS

Enum с несколькими конструкторами

Перечисление может иметь несколько конструкторов. Каждая константа может вызывать разный конструктор:

public enum Color {
    RED(255, 0, 0),
    GREEN(0, 128, 0),
    BLUE(0, 0, 255),
    WHITE(),
    BLACK(0, 0, 0);

    private final int red;
    private final int green;
    private final int blue;

    Color(int r, int g, int b) {
        this.red = r;
        this.green = g;
        this.blue = b;
    }

    Color() {
        this(255, 255, 255);
    }

    public String toHex() {
        return String.format("#%02X%02X%02X", red, green, blue);
    }
}

public class ColorDemo {
    public static void main(String[] args) {
        for (Color c : Color.values()) {
            System.out.println(c + " → " + c.toHex());
        }
    }
}

Вывод программы:

RED → #FF0000
GREEN → #008000
BLUE → #0000FF
WHITE → #FFFFFF
BLACK → #000000

Статические методы в enum

Перечисление может иметь статические методы. Они полезны для поиска констант по какому-то признаку:

import java.util.HashMap;
import java.util.Map;

public enum Currency {
    USD("Доллар США", "$"),
    EUR("Евро", "€"),
    GBP("Фунт стерлингов", "£"),
    JPY("Японская иена", "¥");

    private final String fullName;
    private final String symbol;

    private static final Map<String, Currency> symbolMap = new HashMap<>();

    static {
        for (Currency c : values()) {
            symbolMap.put(c.symbol, c);
        }
    }

    Currency(String fullName, String symbol) {
        this.fullName = fullName;
        this.symbol = symbol;
    }

    public static Currency fromSymbol(String symbol) {
        Currency c = symbolMap.get(symbol);
        if (c == null) {
            throw new IllegalArgumentException("Неизвестный символ: " + symbol);
        }
        return c;
    }

    public String getFullName() {
        return fullName;
    }

    public String getSymbol() {
        return symbol;
    }
}

public class CurrencyDemo {
    public static void main(String[] args) {
        Currency usd = Currency.fromSymbol("$");
        System.out.println(usd + " — " + usd.getFullName());

        for (Currency c : Currency.values()) {
            System.out.println(c.getSymbol() + " — " + c.getFullName());
        }
    }
}

Вывод программы:

USD — Доллар США
$ — Доллар США
€ — Евро
£ — Фунт стерлингов
¥ — Японская иена

Когда использовать enum?

Перечисления идеально подходят для:

  • Дней недели, месяцев, сезонов
  • Состояний объекта (создан, обработан, завершён)
  • Ролей (администратор, пользователь, гость)
  • Операций (сложение, вычитание, умножение)
  • Типов (входящий, исходящий, внутренний)
  • Любого фиксированного набора значений

Не используйте enum когда набор значений может меняться во времени или зависит от внешних данных.

Расширенный пример: игра

Давайте создадим полноценное перечисление для персонажа в игре:

public enum CharacterClass {
    WARRIOR("Воин", 100, 15, 50),
    MAGE("Маг", 60, 25, 80),
    ROGUE("Разбойник", 80, 20, 60),
    HEALER("Целитель", 70, 10, 100);

    private final String russianName;
    private final int maxHp;
    private final int attackPower;
    private final int magicPower;

    CharacterClass(String russianName, int maxHp, int attackPower, int magicPower) {
        this.russianName = russianName;
        this.maxHp = maxHp;
        this.attackPower = attackPower;
        this.magicPower = magicPower;
    }

    public String getRussianName() {
        return russianName;
    }

    public int getMaxHp() {
        return maxHp;
    }

    public int getAttackPower() {
        return attackPower;
    }

    public int getMagicPower() {
        return magicPower;
    }

    public int calculateDamage(boolean isMagic) {
        if (isMagic) {
            return magicPower + (int)(Math.random() * magicPower * 0.3);
        } else {
            return attackPower + (int)(Math.random() * attackPower * 0.3);
        }
    }

    public String getStats() {
        return russianName +
               " | HP: " + maxHp +
               " | Атака: " + attackPower +
               " | Магия: " + magicPower;
    }
}

public class GameDemo {
    public static void main(String[] args) {
        for (CharacterClass cc : CharacterClass.values()) {
            System.out.println(cc.getStats());
        }
        System.out.println();

        CharacterClass hero = CharacterClass.MAGE;
        System.out.println("Выбран: " + hero.getRussianName());
        System.out.println("Урон физический: " + hero.calculateDamage(false));
        System.out.println("Урон магический: " + hero.calculateDamage(true));
    }
}

Вывод программы:

Воин | HP: 100 | Атака: 15 | Магия: 50
Маг | HP: 60 | Атака: 25 | Магия: 80
Разбойник | HP: 80 | Атака: 20 | Магия: 60
Целитель | HP: 70 | Атака: 10 | Магия: 100

Выбран: Маг
Урон физический: 31
Урон магический: 94

Итоги урока

Итоги урока

  • Перечисления (enum) — это типизированный набор именованных констант
  • enum — полноценный класс: имеет поля, конструкторы, методы
  • Конструктор enum всегда private — нельзя создать экземпляр извне
  • Встроенные методы: name(), ordinal(), valueOf(), values()
  • Каждая константа может реализовывать абстрактный метод по-своему
  • EnumSet —高效集合 для перечислений (быстрее HashSet)
  • EnumMap —高效映射 с ключами-перечислениями (быстрее HashMap)
  • enum идеально подходит для switch-конструкций
  • Перечисления могут реализовывать интерфейсы
  • enum создаёт типобезопасный способ работы с фиксированными наборами значений

Следующий урок: Records (Java 16+) →

Тест: enum — перечисления

5 вопросов

Дни недели через enum

Premium