Enum. Практические примеры. Добавление конструкторов и методов

Статья из группы Java Developer
Привет! Сегодня мы поговорим об особом типе данных в Java — Enum (сокращенно от английского enumeration — «перечисление»). В чем же заключается их особенность? Давай представим, что нам нужно реализовать в программе месяцы. Enum. Практические примеры. Добавление конструкторов и методов - 1Казалось бы, в чем проблема? Надо просто определить, какие свойства есть у любого месяца. Пожалуй, нам нужны прежде всего название месяца и число дней в нем. Решение задачи выглядит довольно простым:

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 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 Month JANUARY = new Month("Январь", 31);
   public static Month FEBRUARY = new Month("Февраль", 28);
   public static 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 обратили внимание на существующую проблему. Конечно, здорово, что программисты смогли придумать ее решение с помощью имеющихся в языке средств, но оно выглядит не таким уж простым! Необходимо было очевидное решение, доступное даже новичкам. Так в Java и появился Enum. По сути, Enum — это Java-класс, который предоставляет ограниченный набор значений-объектов. Вот как он выглядит:

public enum Month {
  
   JANUARY,
   FEBRUARY,
   MARCH
}
В определении мы указали что 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, можно использовать? И в чем ее отличие от старой конструкции со 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 для доступа к месяцам. При работе с перечислениями очень важно не забывать, что 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 String name;
   private 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 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 +
               '}';
   }
}
Здесь мы добавили к нашему enum 2 поля — название месяца и число дней, конструктор с использованием этих полей, геттеры-сеттеры, метод toString(), а также 2 статических метода. Как видишь, никаких проблем с этим не возникло: как мы и сказали ранее, 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. Практические примеры. Добавление конструкторов и методов - 2Напоследок хочу порекомендовать тебе одну крайне полезную книгу по Java, а именно — “Effective Java” Джошуа Блоха. Enum. Практические примеры. Добавление конструкторов и методов - 3Автор — один из создателей Java, так что его советам по правильному и грамотному использованию средств языка точно можно доверять :) Применительно к нашей лекции, советую тебе обратить особое внимание на главу книги, посвященную enum. Продуктивного тебе чтения! :)
Комментарии (50)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Сергей Смарт Уровень 43, Одесса, Украина
12 июля 2022
не писать подобную дичь. - как это не писать? Только етим и занимаюсь😄
Марат Гарипов Уровень 48, Россия
27 апреля 2022
В первом же примере, про то как в "Java 1.5 программистам приходилось, откровенно говоря, выкручиваться" с помощью класса и приватного конструктора, сами константы объявлены как public static Month, без final, видимо опечатка
Q1R27 Уровень 17, Ukraine
18 апреля 2022
насколько я понимаю Enum должен быть приват статик константой, что с геттерами и сеттерами неоч сходится, а иначе зачем вообще создавать жесткое ограничение выбора
milyasow Уровень 30, Москва, Russian Federation
20 марта 2022
Не стОит добавлять геттеры и сеттеры в enum...

System.out.println(Month.FEBRUARY); // Month{name='Февраль', daysCount=28}
        Month.FEBRUARY.setDaysCount(10);
        Month.FEBRUARY.setName("Ololo");
        System.out.println(Month.FEBRUARY); // Month{name='Ololo', daysCount=10}
Михаил Уровень 21, Москва, Россия
5 февраля 2022
"Ну, например, старая конструкция не позволяла нам применять свой набор значений в switch-выражениях." Не совсем понял. У меня, к примеру, есть public final class Constants, в котором есть статические публичные константы: и я спокойно могу его использовать в свиче: Т.е. в части enum и моего класса констант (с приватным конструктром, который я явно сделал, чтобы нельзя было создавать объекты) разницы нет. В обоих случаях я спокойно использую в свичах. Что я не так понял? Основная моя идея - сделать классы-справочники, чтобы они содержали уникальные данные (переменные), к котором я могу обращаться, чтобы использовать в своих нуждах. Что в таком случае лучше: енам или справочник-класс-констант? Пытаюсь в реальной жизни определить круг использования енам.
Max S. Уровень 19, Russian Federation
10 ноября 2021
Что-то сильно заморочено. Я ещё туповат для такого...
Laziz Karimov Уровень 26, Noyabrsk
3 ноября 2021
И уносят меня, и уносят меня В звенящую снежную даль Три белых коня эх три белых коня Декабрь, Январь и Февраль
Denis Malyshev Уровень 26, Петрозаводск, Россия
2 сентября 2021
"Основная проблема класса в том что можно создать всякую дичь, поэтому создан enum" так же через два абзаца, в Enum ребята добавляют даже НЕ private конструктор вида:

Month(String name, int daysCount) {
       this.name = name;
       this.daysCount = daysCount;
   }

Чем аннигилируют весь смысл enum 🤨🤨🤨 wtf?
Dina Goncharova Уровень 37
22 июля 2021
Статья супер, спасибо автору!
Jack Daniel Уровень 28, Минск
29 июня 2021
Отличная статья, даже ссылка на покупку хорошей книги есть... За $44 :(. Еще бы ссылку на pdf , и эта статья набрала бы гораздо больше положительных отзывов :).