Что такое перечисления (enums) и в чем их полезность?
Я сегодня просматривал вопросы на StackOverflow и наткнулся на упоминание об использовании enum в шаблоне singleton, где говорилось о предполагаемых преимуществах для потокобезопасности такого решения.
Я никогда раньше не использовал enum, хотя программирую на Java уже более двух лет. Вижу, что они претерпели значительные изменения и теперь поддерживают полноценные принципы ООП внутри себя.
Теперь у меня возникла проблема: зачем и в каких случаях следует использовать enum в повседневном программировании?
5 ответ(ов)
Использование перечислений (enum) всегда является хорошей практикой, когда переменная (особенно параметр метода) может принимать одно из ограниченного набора возможных значений. К таким значениям относятся константы, определяющие типы (например, статус контракта: "постоянный", "временный", "ученик"), или флаги (например, "выполнить сейчас", "отложить выполнение").
Если вы будете использовать перечисления вместо целых чисел (или строковых кодов), это повысит проверку во время компиляции и позволит избежать ошибок, связанных с передачей недопустимых констант. Кроме того, это документирует, какие значения допустимы для использования.
Тем не менее, чрезмерное использование перечислений может означать, что ваши методы делают слишком много — часто лучше иметь несколько отдельных методов, чем один, принимающий несколько флагов, которые изменяют его поведение. Но если необходимо использовать флаги или коды типов, то перечисления являются лучшим выбором.
Рассмотрим пример. Какой подход лучше?
/** Считает количество foobangs.
* @param type Тип foobangs для подсчета. Может быть 1=зеленые foobangs,
* 2=сморщенные foobangs, 3=сладкие foobangs, 0=все типы.
* @return количество foobangs данного типа
*/
public int countFoobangs(int type)
по сравнению с:
/** Типы foobangs. */
public enum FB_TYPE {
GREEN, WRINKLED, SWEET,
/** специальный тип для всех типов вместе */
ALL;
}
/** Считает количество foobangs.
* @param type Тип foobangs для подсчета
* @return количество foobangs данного типа
*/
public int countFoobangs(FB_TYPE type)
Вызов метода, такой как:
int sweetFoobangCount = countFoobangs(3);
теперь становится:
int sweetFoobangCount = countFoobangs(FB_TYPE.SWEET);
Второй вариант сразу показывает, какие типы допустимы, документация и реализация не могут расходиться, и компилятор может это контролировать. Кроме того, недопустимый вызов, такой как:
int sweetFoobangCount = countFoobangs(99);
больше невозможен.
Почему стоит использовать те или иные особенности языков программирования? Причины, по которым существуют языки программирования, следующие:
- Для программистов, чтобы эффективно и корректно выражать алгоритмы в формате, который могут использовать компьютеры.
- Для поддерживающих, чтобы понять алгоритмы, написанные другими, и правильно вносить изменения.
Использование перечислений (enums) повышает вероятность корректности и читаемость кода без необходимости писать много шаблонного кода. Если вы готовы писать шаблонный код, то можете "симулировать" перечисления следующим образом:
public class Color {
private Color() {} // Запрещаем создание экземпляров цветов.
public static final Color RED = new Color();
public static final Color AMBER = new Color();
public static final Color GREEN = new Color();
}
Теперь вы можете написать:
Color trafficLightColor = Color.RED;
Шаблонный код, приведённый выше, имеет тот же эффект, что и
public enum Color { RED, AMBER, GREEN };
Оба варианта обеспечивают одинаковую степень проверки от компилятора. Шаблонный код просто требует больше набора. Однако экономия на наборе делает программиста более эффективным (см. 1), так что это стоит того.
Есть еще одна причина, почему это важно:
Switch-операторы
Одной из вещей, которые "симуляция" static final
перечислений не предоставляет, являются удобные switch
-ветвления. Для типов enum
оператор switch использует тип переменной для определения области видимости случаев перечисления, так что для enum Color
вам нужно лишь написать:
Color color = ... ;
switch (color) {
case RED:
...
break;
}
Обратите внимание, что в случаях не нужно указывать Color.RED
. Если вы не используете перечисления, единственный способ использовать именованные константы с switch
— это что-то похожее на:
public class Color {
public static final int RED = 0;
public static final int AMBER = 1;
public static final int GREEN = 2;
}
Но теперь переменная для хранения цвета должна иметь тип int
. Удобная проверка компилятора для перечислений и симуляции static final
исчезает. Это не очень хорошо.
Компромиссом может быть использование член-значения в симуляции:
public class Color {
public static final int RED_TAG = 1;
public static final int AMBER_TAG = 2;
public static final int GREEN_TAG = 3;
public final int tag;
private Color(int tag) { this.tag = tag; }
public static final Color RED = new Color(RED_TAG);
public static final Color AMBER = new Color(AMBER_TAG);
public static final Color GREEN = new Color(GREEN_TAG);
}
Теперь вы можете написать:
Color color = ... ;
switch (color.tag) {
case Color.RED_TAG:
...
break;
}
Но обратите внимание, что это требует еще больше шаблонного кода!
Использование перечисления как Singleton
Из приведенного выше шаблонного кода видно, почему перечисление предоставляет способ реализации одиночки (singleton). Вместо того, чтобы писать:
public class SingletonClass {
public static final SingletonClass INSTANCE = new SingletonClass();
private SingletonClass() {}
// все методы и данные экземпляра для класса
}
можно написать просто:
public enum SingletonClass {
INSTANCE;
// все методы и данные экземпляра для класса
}
Это дает тот же эффект. Мы можем обойтись без большого количества кода, и это не совсем очевидно, если вы не знакомы с такой идиомой. Мне также не нравится, что вы получаете различные функции перечислений, хотя они не имеют большого смысла для одиночки: ord
, values
и так далее. (Существует на самом деле более сложная симуляция с использованием Color extends Integer
, которая будет работать с switch
, но она такая сложная, что еще более явно показывает, почему enum
— это лучшее решение.)
Потокобезопасность
Потокобезопасность становится потенциальной проблемой только тогда, когда одиночки создаются «лениво» без блокировки.
public class SingletonClass {
private static SingletonClass INSTANCE;
private SingletonClass() {}
public static SingletonClass getInstance() {
if (INSTANCE == null) INSTANCE = new SingletonClass();
return INSTANCE;
}
// все методы и данные экземпляра для класса
}
Если множество потоков одновременно вызовут метод getInstance
, пока INSTANCE
все еще равен null, могут быть созданы множество экземпляров. Это плохо. Единственное решение — добавить синхронизированный доступ к переменной INSTANCE
.
Тем не менее, код с static final
не испытывает этой проблемы. Он создает экземпляр заранее во время загрузки класса. Загрузка классов синхронизирована.
Одноэлементный класс-одиночка (enum
) фактически инициализируется «лениво», так как он инициализируется лишь при первом использовании. Инициализация в Java также синхронизирована, так что несколько потоков не могут инициализировать более одного экземпляра INSTANCE
. Вы получаете лениво инициализируемый одиночку с очень небольшим количеством кода. Единственное негативное — это довольно неочевидный синтаксис. Вам нужно быть знакомым с этой идиомой или хорошенько понимать, как работают загрузка и инициализация классов, чтобы знать, что происходит.
Enums могут быть полезны не только в уже упомянутых сценариях, но и для реализации паттерна "стратегия", следуя основным принципам ООП:
- Код и данные находятся в одном месте — внутри самого enum (или, чаще, внутри его констант, которые могут переопределять методы).
- Реализация интерфейса (или нескольких), чтобы не связывать клиентский код с enum (который должен предоставлять лишь набор стандартных реализаций).
Простой пример — это набор реализаций Comparator
:
enum StringComparator implements Comparator<String> {
NATURAL {
@Override
public int compare(String s1, String s2) {
return s1.compareTo(s2);
}
},
REVERSE {
@Override
public int compare(String s1, String s2) {
return NATURAL.compare(s2, s1);
}
},
LENGTH {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
};
}
Этот "паттерн" может использоваться в более сложных сценариях, широко используя преимущества, которые предоставляет enum: итерацию по экземплярам, опору на их неявный порядок, извлечение экземпляра по имени, статические методы, которые предоставляют правильный экземпляр для конкретного контекста и т. д. При этом все это скрыто за интерфейсом, поэтому ваш код будет работать с пользовательскими реализациями без изменения, если вам потребуется что-то, что не доступно среди "стандартных опций".
Я видел успешное применение этого подхода для моделирования концепции гранулярности времени (дневная, еженедельная и т.д.), где вся логика была инкапсулирована в enum (выбор правильной гранулярности для данного временного диапазона, специфическое поведение, связанное с каждой гранулярностью в виде методов-констант и т. д.). При этом Granularity
, с точки зрения сервисного слоя, была просто интерфейсом.
Полезно знать, что enum
- это всего лишь другой класс с полями Constant
и private
конструктором.
Например:
public enum Weekday
{
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
Компилятор обрабатывает его следующим образом:
class Weekday extends Enum
{
public static final Weekday MONDAY = new Weekday( "MONDAY", 0 );
public static final Weekday TUESDAY = new Weekday( "TUESDAY", 1 );
public static final Weekday WEDNESDAY= new Weekday( "WEDNESDAY", 2 );
public static final Weekday THURSDAY= new Weekday( "THURSDAY", 3 );
public static final Weekday FRIDAY= new Weekday( "FRIDAY", 4 );
public static final Weekday SATURDAY= new Weekday( "SATURDAY", 5 );
public static final Weekday SUNDAY= new Weekday( "SUNDAY", 6 );
private Weekday( String s, int i )
{
super( s, i );
}
// другие методы...
}
Таким образом, можно видеть, что enum
в Java на самом деле является более сложной конструкцией, нежели просто перечисление значений; это полноценный класс с набором статических полей и специфическим конструктором.
Помимо всего, что уже было сказано другими, в одном из старых проектов, с которыми я работал, для общения между независимыми приложениями использовались целые числа, представляющие небольшие наборы значений. В этом контексте было полезно объявить набор как enum
, добавив статические методы для получения объекта enum
по значению и наоборот. Такой подход делал код более читаемым, улучшал использование switch case
и упрощал запись в логи.
Пример кода:
enum ProtocolType {
TCP_IP (1, "Протокол управления передачей"),
IP (2, "Интернет-протокол"),
UDP (3, "Протокол пользовательских датаграмм");
public int code;
public String name;
private ProtocolType(int code, String name) {
this.code = code;
this.name = name;
}
public static ProtocolType fromInt(int code) {
switch(code) {
case 1:
return TCP_IP;
case 2:
return IP;
case 3:
return UDP;
}
// У нас была обработка исключений для этого,
// так как контракт между этими независимыми приложениями
// мог изменяться между версиями (в основном добавление нового функционала),
// но для простоты здесь оставлено null.
return null;
}
}
Создавать объект enum
из полученных значений (например, 1 или 2) можно с помощью ProtocolType.fromInt(2)
. А для записи в логи достаточно использовать myEnumObj.name
.
Надеюсь, это поможет!
Сравнение членов enum в Java: использовать == или equals()?
Цикл for для перебора enum в Java
Можно ли наследовать перечисления для добавления новых элементов?
Как объявить массив в одну строку?
Создание репозитория Spring без сущности