Перечисления (enum) в Java: полное руководство с примерами
Что такое enum в Java
Представьте, что размер кофе хранится в строке: String size = "BIGG";. Опечатка спокойно компилируется, и ошибка всплывёт только тогда, когда клиент не получит свой кофе. Если объявить размер как enum, та же опечатка CoffeeSize.BIGG не пройдёт компиляцию.
Перечисление (enum) в Java — это специальный ссылочный тип (разновидность класса), у которого есть фиксированный заранее известный набор экземпляров — именованных констант. Английское слово enum — сокращение от enumeration, что переводится как «перечисление». Перечисления появились в Java 5 и объявляются с помощью ключевого слова enum.
Перечисления указывают возможные значения для какого-то явления. Например, вы открыли кофейню, в которой продаются три варианта кофе — BIG, HUGE и OVERWHELMING. Других вариантов быть не может. Если задавать значения с помощью String, можно передать любое другое значение, например MIDDLE или SMALL. Задавая перечисление, вы ограничиваете возможные варианты:
public enum CoffeeSize {
BIG, HUGE, OVERWHELMING
} В простейшей форме перечисление — это список именованных констант. Каждая константа (BIG, HUGE и OVERWHELMING) является объектом того перечисления, в котором она определена. Константы неявно объявлены как public static final: их нельзя переприсвоить, и каждая существует в единственном экземпляре.
Какой тип данных у перечисления
Каждое перечисление — это отдельный ссылочный тип данных. Объявив enum CoffeeSize, вы создаёте новый тип CoffeeSize, и переменная этого типа хранит ссылку на одну из его констант. Объекты создаются JVM один раз при загрузке класса перечисления, поэтому оператор new для enum не используется (и не компилируется). В отличие от примитивных типов, переменная перечисления может быть равна null:
public class CoffeeSizeExample1 {
public static void main(String[] args) {
CoffeeSize coffeeSize = CoffeeSize.BIG;
if (coffeeSize == CoffeeSize.BIG) {
System.out.println(coffeeSize); // BIG
}
CoffeeSize notChosen = null; // допустимо: enum - ссылочный тип
}
} Enum против String-констант
До Java 5 перечисления имитировали классом с константами:
public class CoffeeSizeConstants {
public static final String BIG = "BIG";
public static final String HUGE = "HUGE";
public static final String OVERWHELMING = "OVERWHELMING";
} Такой подход работает, но у enum гораздо больше преимуществ:
| Критерий | Константы String/int | enum |
|---|---|---|
| Проверка типов | Нет: метод примет любую строку | Да: можно передать только константу этого enum |
| Перебор всех значений | Вручную | Метод values() |
| Поля, конструкторы, методы | Нет | Да |
| Использование в switch | Да, но без контроля полноты | Да, компилятор проверяет полноту в switch-выражении |
| Специализированные коллекции | Нет | EnumSet, EnumMap |
Enum в операторе switch
Значения перечисления можно использовать в операторе switch. В ветвях case должны использоваться константы того же перечисления, что и в выражении switch. Имена констант указываются без имени типа: тип перечисления в switch уже неявно задаёт тип для меток case.
public class CoffeeSizeExample2 {
public static void main(String[] args) {
CoffeeSize coffeeSize = CoffeeSize.BIG;
switch (coffeeSize) {
case BIG:
System.out.println("Дайте мне большую чашку кофе!");
break;
case HUGE:
System.out.println("Дайте мне огромную чашку кофе!");
break;
case OVERWHELMING:
System.out.println("Дайте мне громадную чашку кофе!");
break;
default:
System.out.println("Чашка не выбрана.");
}
}
} С Java 14 удобнее использовать switch-выражение со стрелками. Если перечислены все константы, ветка default не нужна: компилятор сам проверит полноту и выдаст ошибку, если в enum добавят новую константу, а switch забудут обновить.
String message = switch (coffeeSize) {
case BIG -> "Большая чашка";
case HUGE -> "Огромная чашка";
case OVERWHELMING -> "Громадная чашка";
}; Нюанс
Если переменная enum равна null, классический switch выбросит NullPointerException — ветка default не спасёт. До Java 21 в case разрешено только простое имя константы (case BIG), запись case CoffeeSize.BIG не компилировалась; начиная с Java 21 квалифицированное имя тоже допустимо.
Класс java.lang.Enum и методы enum
Все перечисления неявно наследуют класс java.lang.Enum: компилятор сам добавляет extends Enum<CoffeeSize>. Поэтому явно написать extends у перечисления нельзя — в Java нет множественного наследования классов. Унаследовать перечисление обычным классом тоже нельзя: enum неявно final. Единственное исключение — константы с собственным телом (см. раздел о переопределении): для них компилятор создаёт анонимные подклассы, а сам enum становится неявно sealed (Java 17+).
Класс Enum реализует интерфейсы Comparable и Serializable, поэтому любое перечисление можно сравнивать и сериализовать. В Enum определены методы name(), ordinal(), compareTo(), equals(), toString() и другие. Статические методы values() и valueOf(String) в классе Enum не объявлены — компилятор добавляет их в каждое перечисление автоматически.
| Метод | Что возвращает | Пример для CoffeeSize.HUGE |
|---|---|---|
values() | Массив всех констант в порядке объявления | [BIG, HUGE, OVERWHELMING] |
valueOf(String) | Константу по точному имени | valueOf("HUGE") → HUGE |
name() | Имя константы, как в коде | "HUGE" |
toString() | По умолчанию то же, что name(), можно переопределить | "HUGE" |
ordinal() | Порядковый номер, начиная с 0 | 1 |
compareTo(e) | Разницу порядковых номеров | HUGE.compareTo(BIG) → 1 |
equals(o) | true, только если это та же константа | HUGE.equals(CoffeeSize.HUGE) → true |
Метод values()
Метод values() возвращает массив, содержащий все константы перечисления в порядке их объявления:
public class CoffeeSizeValuesExample {
public static void main(String[] args) {
System.out.println("Константы перечислимого типа CoffeeSize:");
CoffeeSize[] coffeeSizes = CoffeeSize.values();
for (CoffeeSize c : coffeeSizes) {
System.out.println(c);
}
}
} Результат выполнения:
Константы перечислимого типа CoffeeSize:
BIG
HUGE
OVERWHELMING Каждый вызов values() создаёт новую копию массива, поэтому изменение возвращённого массива не повлияет на перечисление. В горячем цикле результат лучше сохранить в переменную.
Метод valueOf()
Статический метод valueOf() возвращает константу, имя которой совпадает со строкой-аргументом. Иначе говоря, он преобразует String в enum:
public class CoffeeSizeExample3 {
public static void main(String[] args) {
CoffeeSize coffeeSize = CoffeeSize.valueOf("BIG");
System.out.println("Переменная coffeeSize содержит " + coffeeSize);
CoffeeSize wrong = CoffeeSize.valueOf("big"); // IllegalArgumentException
}
} Имя сравнивается с учётом регистра и без обрезки пробелов. Если такой константы нет, выбрасывается IllegalArgumentException, а при передаче null — NullPointerException.
Методы name() и toString()
Метод name() объявлен как final и всегда возвращает имя константы точно так, как оно записано в коде. Метод toString() по умолчанию возвращает то же самое, но его можно переопределить, например, чтобы выводить название для пользователя. Для надёжного преобразования enum в строку и обратно (через valueOf()) используйте именно name().
Метод ordinal()
Метод ordinal() возвращает позицию константы в объявлении перечисления. Нумерация начинается с нуля:
public class CoffeeSizeExample4 {
public static void main(String[] args) {
for (CoffeeSize c : CoffeeSize.values()) {
System.out.println(c + " " + c.ordinal());
}
}
} BIG 0
HUGE 1
OVERWHELMING 2 Метод compareTo()
С помощью метода int compareTo(E e) можно сравнить порядковые значения двух констант одного и того же перечисления. Сравнить константы разных перечислений не получится — это ошибка компиляции.
- Если порядковое значение вызывающей константы меньше, чем у
e, возвращается отрицательное число. - Если это одна и та же константа, возвращается ноль.
- Если порядковое значение вызывающей константы больше, возвращается положительное число.
public class CoffeeSizeCompareTo {
public static void main(String[] args) {
CoffeeSize bigCup = CoffeeSize.BIG;
CoffeeSize hugeCup = CoffeeSize.HUGE;
CoffeeSize anotherBigCup = CoffeeSize.BIG;
CoffeeSize overwhelmingCup = CoffeeSize.OVERWHELMING;
System.out.println("bigCup.compareTo(hugeCup): " + bigCup.compareTo(hugeCup));
System.out.println("hugeCup.compareTo(bigCup): " + hugeCup.compareTo(bigCup));
System.out.println("bigCup.compareTo(anotherBigCup): " + bigCup.compareTo(anotherBigCup));
System.out.println("bigCup.compareTo(overwhelmingCup): " + bigCup.compareTo(overwhelmingCup));
}
} Результат выполнения кода:
bigCup.compareTo(hugeCup): -1
hugeCup.compareTo(bigCup): 1
bigCup.compareTo(anotherBigCup): 0
bigCup.compareTo(overwhelmingCup): -2 В текущей реализации compareTo() возвращает разность this.ordinal() - e.ordinal(), отсюда значение -2. Метод объявлен как final, поэтому естественный порядок констант всегда совпадает с порядком объявления.
Метод equals() и оператор ==
Метод equals() в Enum переопределяет метод из класса Object и объявлен как final. Две константы равны, только если это одна и та же константа одного и того же перечисления. Совпадение порядковых номеров у констант разных перечислений не делает их равными.
Поскольку каждая константа существует в единственном экземпляре, для сравнения можно использовать оператор == — результат будет тем же, что и у equals().
public class CoffeeSizeEquals {
public static void main(String[] args) {
CoffeeSize bigCup = CoffeeSize.BIG;
CoffeeSize hugeCup = CoffeeSize.HUGE;
CoffeeSize anotherBigCup = CoffeeSize.BIG;
System.out.println("bigCup.equals(hugeCup): " + bigCup.equals(hugeCup));
System.out.println("bigCup.equals(anotherBigCup): " + bigCup.equals(anotherBigCup));
System.out.println("bigCup == anotherBigCup: " + (bigCup == anotherBigCup));
}
} bigCup.equals(hugeCup): false
bigCup.equals(anotherBigCup): true
bigCup == anotherBigCup: true Совет
Для enum оператор == даже предпочтительнее equals(): он не выбросит NullPointerException, если слева null, а сравнение констант разных перечислений через == компилятор отклонит сразу.
Enum со значениями: поля, конструктор и методы
Создать экземпляр перечисления с помощью оператора new нельзя, но в остальном перечисление обладает возможностями обычного класса: в него можно добавлять конструкторы, поля и методы. Так получается enum со значениями.
Допустим, мы хотим задать объём чашки кофе в миллилитрах. Для этого добавим в перечисление поле ml, метод-геттер getMl() и конструктор, который принимает значение объёма.
Конструктор перечисления всегда private: если модификатор не указан, он подразумевается, а объявить конструктор как public или protected — ошибка компиляции. Как же тогда его вызвать? После имени константы ставятся круглые скобки с аргументами — так константа создаётся через нужный конструктор. Список констант должен идти первым в теле enum и заканчиваться точкой с запятой, если за ним следуют другие члены.
public enum CoffeeSize2 {
// 100, 150 и 200 передаются в конструктор
BIG(100), HUGE(150), OVERWHELMING(200);
private final int ml;
CoffeeSize2(int ml) {
this.ml = ml;
}
public int getMl() {
return ml;
}
} Методы перечисления вызываются так же, как и у обычного объекта. В следующем классе мы перебираем все константы и для каждой вызываем getMl():
public class CoffeeSizeMlExample {
public static void main(String[] args) {
for (CoffeeSize2 coffeeSize : CoffeeSize2.values()) {
System.out.println(coffeeSize + " " + coffeeSize.getMl());
}
}
} BIG 100
HUGE 150
OVERWHELMING 200 Конструкторы в перечислении можно перегружать. Чтобы вызвать конструктор без параметров, не пишите скобки после константы:
public enum CoffeeSize3 {
BIG(100), HUGE, OVERWHELMING(200);
private final int ml;
CoffeeSize3(int ml) {
this.ml = ml;
}
CoffeeSize3() {
this.ml = -1;
}
public int getMl() {
return ml;
}
} Где можно объявить перечисление
Перечисление можно объявить отдельным типом верхнего уровня (в своём файле) или как член класса. Начиная с Java 16 (JEP 395) перечисления можно объявлять и локально — внутри метода, конструктора или блока. Вложенные и локальные перечисления неявно static, поэтому не имеют доступа к полям экземпляра внешнего класса.
В этом примере перечисление CoffeeSize объявлено внутри класса Coffee3:
public class Coffee3 {
enum CoffeeSize { BIG, HUGE, OVERWHELMING }
CoffeeSize size;
} Для обращения к такому перечислению из другого класса используется имя внешнего класса:
public class CoffeeTest2 {
public static void main(String[] args) {
Coffee3 drink = new Coffee3();
drink.size = Coffee3.CoffeeSize.BIG; // имя внешнего класса необходимо
}
} Объявление перечисления внутри метода (Java 16+):
public class CoffeeTest3 {
public static void main(String[] args) {
enum CoffeeSize { BIG, HUGE, OVERWHELMING }
CoffeeSize coffeeSize = CoffeeSize.BIG;
}
} Переопределение методов в константах
В перечислении можно переопределять методы, но это не совсем обычное переопределение: метод переопределяется не в подклассе, а в теле конкретной константы.
Добавим в перечисление метод getLidCode(), который возвращает код крышки для чашки кофе. Для всех констант подходит код B, кроме OVERWHELMING — для неё нужен код A. После объявления константы открываем фигурные скобки и переопределяем в них метод. Если нужно переопределить несколько методов, это делается в тех же фигурных скобках.
public enum CoffeeSize4 {
BIG(100),
HUGE(150),
OVERWHELMING(200) {
@Override
public String getLidCode() {
return "A";
}
};
private final int ml;
CoffeeSize4(int ml) {
this.ml = ml;
}
public int getMl() {
return ml;
}
public String getLidCode() {
return "B";
}
} public class CoffeeSizeExample7 {
public static void main(String[] args) {
for (CoffeeSize4 coffeeSize : CoffeeSize4.values()) {
System.out.println(coffeeSize + " " + coffeeSize.getLidCode());
}
}
} Результат выполнения кода:
BIG B
HUGE B
OVERWHELMING A Если у каждой константы своё поведение, метод можно объявить абстрактным — тогда компилятор потребует реализовать его в каждой константе:
public enum Operation {
PLUS {
public int apply(int a, int b) { return a + b; }
},
MINUS {
public int apply(int a, int b) { return a - b; }
};
public abstract int apply(int a, int b);
} Неочевидный момент
Константа с телом — это экземпляр анонимного подкласса. Поэтому CoffeeSize4.OVERWHELMING.getClass() вернёт не CoffeeSize4, а класс вида CoffeeSize4$1. Чтобы получить сам тип перечисления, используйте getDeclaringClass().
Перечисления и интерфейсы
Перечисления не могут явно наследовать другие классы, но могут реализовывать любое количество интерфейсов. Методы интерфейса можно реализовать один раз для всего enum или отдельно в теле каждой константы:
public interface HasPrice {
int priceInCents();
}
public enum Currency implements HasPrice {
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
private final int value;
Currency(int value) {
this.value = value;
}
@Override
public int priceInCents() {
return value;
}
} public class CurrencyExample {
public static void main(String[] args) {
HasPrice coin = Currency.QUARTER;
System.out.println(coin.priceInCents()); // 25
}
} Так же можно реализовать и стандартные интерфейсы, например Runnable: тогда константу перечисления можно передать прямо в конструктор Thread.
EnumSet и EnumMap
Для работы с перечислениями в пакете java.util есть две специализированные коллекции. Они работают быстрее и занимают меньше памяти, чем HashSet и HashMap, потому что опираются на порядковые номера констант:
EnumSet— множество констант одного enum, внутри хранится как битовая маска;EnumMap— отображение, ключами которого являются константы enum, внутри это массив, индексированный поordinal().
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
public class EnumCollectionsExample {
public static void main(String[] args) {
Set<CoffeeSize> large = EnumSet.of(CoffeeSize.HUGE, CoffeeSize.OVERWHELMING);
System.out.println(large.contains(CoffeeSize.BIG)); // false
Map<CoffeeSize, Integer> prices = new EnumMap<>(CoffeeSize.class);
prices.put(CoffeeSize.BIG, 150);
prices.put(CoffeeSize.HUGE, 200);
System.out.println(prices); // {BIG=150, HUGE=200}
}
} Частые ошибки при работе с enum
- Хранение
ordinal()в базе данных или файле. Порядковый номер меняется, если добавить константу в середину или поменять порядок. Хранитеname()или собственное поле-код. valueOf()для пользовательского ввода без проверки. Строка"big"или"BIG "приведёт кIllegalArgumentException. Нормализуйте ввод или обрабатывайте исключение.- Попытка написать
new CoffeeSize()илиpublic-конструктор. Оба варианта не компилируются. - Попытка написать
enum A extends B. Перечисление уже наследуетjava.lang.Enum; вместо наследования используйте интерфейсы. - Изменяемые поля в enum. Константа одна на всё приложение, поэтому изменяемое поле фактически становится глобальным состоянием. Делайте поля
final.
Часто задаваемые вопросы
Какой тип данных используется для перечисления в Java?
Каждое перечисление — это самостоятельный ссылочный тип данных, особый вид класса, который неявно наследует java.lang.Enum. Переменная типа enum хранит ссылку на одну из констант и может быть равна null. Внутри константы нумеруются целыми числами (ordinal()), но сам enum к типу int не приводится.
Чем enum отличается от интерфейса java.util.Enumeration?
Это разные вещи с похожими названиями. enum — ключевое слово для объявления перечисления. java.util.Enumeration — устаревший интерфейс для перебора элементов коллекций вроде Vector и Hashtable (методы hasMoreElements() и nextElement()); в новом коде вместо него используют Iterator.
Почему enum считают лучшим способом реализовать Singleton?
Перечисление с одной константой (enum Registry { INSTANCE; }) гарантирует единственный экземпляр: JVM создаёт его потокобезопасно при инициализации класса, рефлексия не может вызвать конструктор enum, а при десериализации возвращается та же константа. Такой подход рекомендует Джошуа Блох в книге «Effective Java».
Как правильно сохранять enum в базу данных?
Надёжнее всего хранить строку из name() или собственный стабильный код из поля перечисления. Число из ordinal() сохранять не стоит: после добавления или перестановки констант старые записи начнут указывать на другие значения. В JPA для этого используют @Enumerated(EnumType.STRING).
Можно ли унаследовать один enum от другого?
Нет. Перечисление уже неявно наследует java.lang.Enum, а множественного наследования классов в Java нет, поэтому extends у enum не пишется. Унаследоваться от enum тоже нельзя — он неявно final (подклассами бывают только анонимные тела констант). Общее поведение нескольких перечислений выносят в интерфейс.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии