Привет! Сегодня мы поговорим об особом типе данных в Java: это
Enum (сокращенно от английского enumeration, «перечисление»).
Если нужен короткий ответ:
enum это особый вид класса, у которого заранее задан фиксированный список объектов-констант, например двенадцать месяцев или семь дней недели. Создать значение сверх этого списка компилятор не даст. Константы можно перебирать методом
values() и подставлять прямо в
switch, а внутри самого
enum разрешены поля, конструктор и обычные методы, ведь это полноценный класс.
Кратко
enum это класс с заранее заданным списком объектов-констант. Ничего сверх этого списка создать нельзя, за этим следит компилятор.- Появился в Java 5. До нее тот же результат собирали вручную: приватный конструктор плюс набор полей
public static final. - Каждая константа это статический объект. Обращаются к ней по имени типа,
Month.JANUARY, создавать ничего не нужно. - Значение
enum можно подставить в switch. Со старыми константами так не получалось, компилятор выдавал ошибку. - Компилятор сам наследует любой
enum от java.lang.Enum, поэтому унаследовать его еще от одного класса нельзя. А вот реализовать интерфейс можно. - Внутри
enum разрешены поля, конструктор и методы, в том числе статические. Конструктор всегда приватный, даже если модификатор не написан.
В чем же заключается их особенность?
Давай представим, что нам нужно реализовать в программе месяцы.
![Гигантский календарь, нарисованный на брусчатке городской площади: крупные числа дней и расписание событий, по нему идут люди]()
Казалось бы, в чем проблема? Надо просто определить, какие свойства есть у любого месяца. Пожалуй, нам нужны прежде всего название месяца и число дней в нем.
Решение задачи выглядит довольно простым:
public class Month {
private String name;
private int daysCount;
public Month(String name, int daysCount) {
this.name = name;
this.daysCount = daysCount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getDaysCount() {
return daysCount;
}
public void setDaysCount(int daysCount) {
this.daysCount = daysCount;
}
@Override
public String toString() {
return "Month{" +
"name='" + name + '\'' +
", daysCount=" + daysCount +
'}';
}
}
Пожалуйста, полный набор!
У нас есть класс
Month, нужные поля, геттеры-сеттеры,
toString(). Разве что
equals() и hashCode() надо добавить для полного счастья :) Однако, у нас есть концептуальная проблема.
Проблема: у сущности бывает строго ограниченный набор значений
Как ты, наверное, помнишь, одно из главных преимуществ ООП заключается в том, что оно позволяет легко моделировать сущности из реального мира.
Стул, машина, планета: все эти понятия из обычной жизни легко представить в программе при помощи абстракции.
Проблема в том, что у некоторых сущностей в реальном мире есть строго ограниченный круг значений.
В году всего 4 времени года.
В музыке всего 7 нот.
В календаре всего 12 месяцев.
У Оушена всего 11 друзей (хотя, это спорный вопрос :))
Иными словами, обычный Java-класс не способен смоделировать эти сущности и соблюсти их естественные ограничения.
В нашем классе
Month есть все нужные поля. Но ведь если его будет использовать другой программист, никто не помешает ему создавать совершенно безумные объекты:
public class Main {
Month month1 = new Month("lolkek", 322);
Month month2 = new Month("yahoooooooooooo", 12345);
}
Если такое появится в программе, будет непросто найти виновного! С одной стороны, программист, создавший объекты, мог бы и понять, что класс
Month подразумевает «месяц в году» и не писать подобную дичь.
С другой стороны, он всего лишь пользовался теми возможностями, которые ему предоставил проектировщик класса. Можно назначать любые имена и количество дней? Он и назначил.
Что же в такой ситуации делать?
Как ограниченный набор значений собирали до Java 5
До выхода версии языка Java 1.5 программистам приходилось, откровенно говоря, выкручиваться :)
В те времена они создавали вот такие конструкции:
public class Month {
private String name;
private int daysCount;
private Month(String name, int daysCount) {
this.name = name;
this.daysCount = daysCount;
}
public static final Month JANUARY = new Month("Январь", 31);
public static final Month FEBRUARY = new Month("Февраль", 28);
public static final Month MARCH = new Month("Март", 31);
@Override
public String toString() {
return "Month{" +
"name='" + name + '\'' +
", daysCount=" + daysCount +
'}';
}
}
Здесь мы упростили количество месяцев до трех вместо двенадцати, чтобы пример был короче.
Такие конструкции позволяли решить поставленную задачу. Варианты создаваемых объектов ограничивались приватным конструктором:
private Month(String name, int daysCount) {
this.name = name;
this.daysCount = daysCount;
}
Программисты, использующие класс, не могли просто создавать объекты
Month. Они вынуждены были пользоваться теми
финальными статическими объектами, которые предоставлял разработчик класса.
Это выглядело примерно вот так:
public class Main {
public static void main(String[] args) {
Month january = Month.JANUARY;
System.out.println(january);
}
}
Однако разработчики Java обратили внимание на существующую проблему. Конечно, здорово, что программисты смогли придумать ее решение с помощью имеющихся в языке средств, но оно выглядит не таким уж простым! Необходимо было очевидное решение, доступное даже новичкам.
Что такое enum в Java
Так в Java и появился
Enum.
По сути,
Enum это Java-класс, который предоставляет ограниченный набор значений-объектов.
Вот как он выглядит:
public enum Month {
JANUARY,
FEBRUARY,
MARCH
}
Почему enum нельзя унаследовать от другого класса
В определении мы указали, что
Enum это Java-класс, но действительно ли это так? Да, и мы даже можем это проверить.
Попробуй, например, унаследовать наш
enum Month от какого-то другого класса:
public abstract class AbstractMonth {
}
public enum Month extends AbstractMonth {
JANUARY,
FEBRUARY,
MARCH
}
Почему так происходит?
Когда мы пишем в программе:
public enum Month
компилятор преобразует эту команду в такой код:
public class Month extends Enum
Как ты уже знаешь, множественное наследование в Java запрещено. Поэтому и наследоваться от
AbstractMonth мы не смогли.
Enum в switch
Как эту новую конструкцию,
Enum, можно использовать? И в чем ее отличие от старой конструкции со
static final полями?
Ну, например, старая конструкция не позволяла нам применять свой набор значений в
switch-выражениях.
Представь, что мы хотим создать программу, которая будет напоминать нам о том, какие праздники отмечаются в этом месяце:
public class HolidayReminder {
public void printHolidays(Month month) {
switch (month) {
case JANUARY:
}
}
}
Здесь, как видишь, компилятор выбрасывает ошибку.
Но после того, как в Java 1.5 появились
enum, все стало гораздо проще:
public enum Month {
JANUARY,
FEBRUARY,
MARCH
}
public class HolidayReminder {
public void printHolidays(Month month) {
switch (month) {
case JANUARY:
System.out.println("7 января будет Рождество!");
break;
case FEBRUARY:
System.out.println("В феврале празднуется День Защитника Отечества - 23 февраля!");
break;
case MARCH:
System.out.println("В марте отмечается Всемирный Женский День - 8 марта!");
break;
}
}
}
public class Main {
public static void main(String[] args) {
HolidayReminder reminder = new HolidayReminder();
reminder.printHolidays(Month.JANUARY);
}
}
Вывод в консоль:
7 января будет Рождество!
Обрати внимание: доступ к объектам
Enum остался статическим, как это было до Java 1.5. Нам не нужно создавать объект
Month для доступа к месяцам.
Кстати, сам
switch с тех пор тоже подтянулся: с Java 14 у него есть стрелочная форма,
case JANUARY -> System.out.println("7 января будет Рождество!");. Она не проваливается в следующую ветку, поэтому
break писать не нужно. На работу с
enum это никак не влияет, просто короче.
Enum и старые static-константы: в чем разница
Одну разницу мы уже увидели, но она не единственная. Вот что
enum дает бесплатно, а что со старыми константами пришлось бы писать руками.
| Возможность | Константы public static final | enum |
|---|
Работает в switch | нет | да |
| Список всех значений | надо собирать вручную | готовый метод values() |
| Значение по имени-строке | надо писать самому | готовый метод valueOf("JANUARY") |
| Имя константы строкой | надо хранить отдельным полем | name(), и toString() по умолчанию возвращает его же |
| Специальные коллекции | нет, только обычные HashSet и HashMap | EnumSet и EnumMap, они компактнее и быстрее |
| Защита от лишних значений | держится на дисциплине: конструктор надо не забыть сделать приватным | на уровне языка, других объектов создать не выйдет |
Конструкторы, поля и методы в enum
При работе с перечислениями очень важно не забывать, что
Enum это полноценный класс.
Это значит, что при необходимости ты можешь определить в нем конструкторы и методы.
К примеру, в предыдущем куске кода мы просто указали значения JANUARY, FEBRUARY, MARCH.
Однако мы можем расширить наш
enum Month вот таким образом:
public enum Month {
JANUARY("Январь", 31),
FEBRUARY("Февраль", 28),
MARCH("Март", 31),
APRIL("Апрель", 30),
MAY("Май", 31),
JUNE("Июнь", 30),
JULY("Июль", 31),
AUGUST("Август", 31),
SEPTEMBER("Сентябрь", 30),
OCTOBER("Октябрь", 31),
NOVEMBER("Ноябрь", 30),
DECEMBER("Декабрь", 31);
private final String name;
private final int daysCount;
Month(String name, int daysCount) {
this.name = name;
this.daysCount = daysCount;
}
public static Month[] getWinterMonths() {
return new Month[]{DECEMBER, JANUARY, FEBRUARY};
}
public static Month[] getSummerMonths() {
return new Month[]{JUNE, JULY, AUGUST};
}
public String getName() {
return name;
}
public int getDaysCount() {
return daysCount;
}
@Override
public String toString() {
return "Month{" +
"name='" + name + '\'' +
", daysCount=" + daysCount +
'}';
}
}
Здесь мы добавили к нашему
enum 2 поля: название месяца и число дней, конструктор с использованием этих полей, геттеры, метод
toString(), а также 2 статических метода.
Обрати внимание на два момента. Первый: список констант закрывается точкой с запятой, без нее компилятор не поймет, где кончается перечисление и начинается обычный код класса. Второй: поля объявлены как
final и сеттеров у них нет, и это не случайность. Каждая константа
enum существует в единственном экземпляре на всю программу, так что сеттер позволил бы любому куску кода переименовать
JANUARY или выставить февралю 300 дней сразу для всех. Джошуа Блох в «Effective Java», которую мы советуем ниже, рекомендует делать поля перечислений неизменяемыми именно по этой причине.
Как видишь, никаких проблем с этим не возникло: как мы и сказали ранее,
enum это полноценный класс:
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
System.out.println(Arrays.toString(Month.getSummerMonths()));
}
}
Вывод в консоль:
[Month{name='Июнь', daysCount=30}, Month{name='Июль', daysCount=31}, Month{name='Август', daysCount=31}]
Встроенные методы enum
Часть методов компилятор дописывает в каждый
enum сам, руками их создавать не нужно. Самые ходовые это
values() и
valueOf().
public class Main {
public static void main(String[] args) {
for (Month month : Month.values()) {
System.out.println(month.getName() + ": " + month.getDaysCount());
}
Month march = Month.valueOf("MARCH");
System.out.println(march.name() + ", номер " + march.ordinal());
}
}
Вывод в консоль:
Январь: 31
Февраль: 28
Март: 31
Апрель: 30
Май: 31
Июнь: 30
Июль: 31
Август: 31
Сентябрь: 30
Октябрь: 31
Ноябрь: 30
Декабрь: 31
MARCH, номер 2
И пара ловушек напоследок.
valueOf() ищет строгое совпадение с именем константы, а не со значением поля
name: передашь ему «Март» или «march», получишь
IllegalArgumentException. А
name() переопределить нельзя, он всегда вернет имя константы, даже если
toString() в классе переписан, как у нас. И последнее: на
ordinal() лучше не завязывать логику, ведь стоит кому-то вставить новую константу в середину списка, и все номера съедут.
Напоследок хочу порекомендовать тебе одну крайне полезную книгу по Java, а именно
“Effective Java” Джошуа Блоха.
![Обложка книги Effective Java, третье издание, Джошуа Блоха рядом с его автографом]()
Автор книги один из создателей Java, так что его советам по правильному и грамотному использованию средств языка точно можно доверять :)
Применительно к нашей лекции, советую тебе обратить особое внимание на главу книги, посвященную
enum.
Продуктивного тебе чтения! :)
Вопросы и ответы
Что такое enum в Java?
Это особый вид класса с заранее заданным списком объектов-констант. Список фиксирован, значение вне его создать нельзя. Типичные примеры: месяцы, дни недели, статусы заказа, масти карт.
Чем enum лучше набора констант public static final?
Набор значений ограничивает сам компилятор, такие константы работают в
switch, а еще из коробки есть
values(),
valueOf() и специальные коллекции
EnumSet и
EnumMap. Со старыми константами все это приходилось писать руками.
Как перебрать все значения enum?
Методом
values(). Компилятор неявно добавляет его в каждый
enum, и метод возвращает массив констант в том порядке, в котором они объявлены:
for (Month m : Month.values()).
Можно ли унаследовать enum от другого класса?
Нет. Компилятор уже наследует любой
enum от
java.lang.Enum, а множественное наследование классов в Java запрещено. Реализовать интерфейс
enum при этом может, и не один.
Можно ли добавить в enum поля, конструктор и методы?
Да, это полноценный класс. Поля и методы пишутся после списка констант, а сам список нужно закрыть точкой с запятой. Конструктор в
enum всегда приватный, даже если модификатор не написан, поэтому вызвать его извне не получится.
С какой версии Java появился enum?
С Java 5, она же 1.5, вышедшей в 2004 году. До этого ограниченный набор значений собирали вручную: приватный конструктор плюс статические финальные поля с готовыми объектами.
Источник:
Oracle Java Tutorials: Enum Types.
Читайте также