Привет! Сегодня мы поговорим об особом типе данных в 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 {
}

//ошибка! No extends clause allowed to enum
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 finalenum
Работает в switchнетда
Список всех значенийнадо собирать вручнуюготовый метод values()
Значение по имени-строкенадо писать самомуготовый метод valueOf("JANUARY")
Имя константы строкойнадо хранить отдельным полемname(), и toString() по умолчанию возвращает его же
Специальные коллекциинет, только обычные HashSet и HashMapEnumSet и 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) {

       //values() возвращает массив всех констант в порядке объявления
       for (Month month : Month.values()) {
           System.out.println(month.getName() + ": " + month.getDaysCount());
       }

       //valueOf() находит константу по ее имени
       Month march = Month.valueOf("MARCH");

       //name() возвращает имя константы, ordinal() ее номер в списке
       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.

Читайте также