enum — Перечисления в Java
Знакомимся с перечислениями (enum): константы, поля, методы, конструкторы, switch, EnumSet и EnumMap — мощные инструменты для типизированных наборов значений
Зачем нужны перечисления?
Представьте, что вы разрабатываете программу для управления светофором. У светофора есть три состояния: красный, жёлтый и зелёный. Как бы вы описали эти состояния в 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 вопросов