Вопрос-ответ

What are enums and why are they useful?

Что такое перечисления и почему они полезны?

Сегодня я просматривал несколько вопросов на этом сайте и нашел упоминание о перечислении используемом в singleton pattern, о предполагаемых преимуществах потокобезопасности такого решения.

Я никогда не использовал перечисления и программирую на Java уже больше пары лет. И, по-видимому, они сильно изменились. Теперь они даже осуществляют полноценную поддержку ООП внутри себя.

Теперь, почему и для чего я должен использовать enum в повседневном программировании?

Переведено автоматически
Ответ 1

Вы всегда должны использовать перечисления, когда переменная (особенно параметр метода) может принимать только одно из небольшого набора возможных значений. Примерами могут служить такие вещи, как константы типа (статус контракта: "постоянный", "временный", "ученический") или флаги ("выполнить сейчас", "отложить выполнение").

Если вы используете перечисления вместо целых чисел (или строковых кодов), вы увеличиваете время проверки во время компиляции и избегаете ошибок при передаче недопустимых констант, а также документируете, какие значения допустимы для использования.

Кстати, чрезмерное использование перечислений может означать, что ваши методы делают слишком много (часто лучше иметь несколько отдельных методов, а не один метод, который принимает несколько флагов, которые изменяют то, что он делает), но если вам нужно использовать флаги или коды типов, перечисления - это правильный путь.

В качестве примера, что лучше?

/** Counts number of foobangs.
* @param type Type of foobangs to count. Can be 1=green foobangs,
* 2=wrinkled foobangs, 3=sweet foobangs, 0=all types.
* @return number of foobangs of type
*/

public int countFoobangs(int type)

против

/** Types of foobangs. */
public enum FB_TYPE {
GREEN, WRINKLED, SWEET,
/** special type for all types combined */
ALL;
}

/** Counts number of foobangs.
* @param type Type of foobangs to count
* @return number of foobangs of type
*/

public int countFoobangs(FB_TYPE type)

Вызов метода типа:

int sweetFoobangCount = countFoobangs(3);

затем становится:

int sweetFoobangCount = countFoobangs(FB_TYPE.SWEET);

Во втором примере сразу становится ясно, какие типы разрешены, документация и реализация не могут не синхронизироваться, и компилятор может обеспечить это.
Кроме того, недопустимый вызов типа

int sweetFoobangCount = countFoobangs(99);

больше невозможно.

Ответ 2

Зачем использовать какие-либо функции языка программирования? Причина, по которой у нас вообще есть языки, заключается в


  1. Программисты должны эффективно и правильно выражать алгоритмы в форме, доступной компьютерам.

  2. Сопровождающие должны понимать алгоритмы, написанные другими, и правильно вносить изменения.

Перечисления повышают как вероятность корректности, так и удобочитаемость без написания большого количества шаблонов. Если вы готовы писать шаблонно, то вы можете "имитировать" перечисления:

public class Color {
private Color() {} // Prevent others from making colors.
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 };

Оба предоставляют одинаковый уровень помощи в проверке от компилятора. Boilerplate - это просто больший набор текста. Но экономия большого количества текста делает программиста более эффективным (см. 1), так что это полезная функция.

Это полезно по крайней мере еще по одной причине:

Операторы переключения

Единственное, чего static final приведенное выше моделирование перечислений не дает вам, - это приятные switch случаи. Для типов перечислений Java switch использует тип своей переменной для определения области видимости случаев перечисления, поэтому для enum Color выше вам просто нужно сказать:

Color color = ... ;
switch (color) {
case RED:
...
break;
}

Обратите внимание, что это не Color.RED в падежах. Если вы не используете enum, единственный способ использовать именованные величины с 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;
}

Но обратите внимание, что это еще более шаблонно!

Использование перечисления в качестве синглтона

Из приведенного выше шаблона вы можете увидеть, почему перечисление предоставляет способ реализации синглтона. Вместо записи:

public class SingletonClass {
public static final void INSTANCE = new SingletonClass();
private SingletonClass() {}

// all the methods and instance data for the class here
}

а затем получить к нему доступ с помощью

SingletonClass.INSTANCE

мы можем просто сказать

public enum SingletonClass {
INSTANCE;

// all the methods and instance data for the class here
}

что дает нам то же самое. Это сойдет нам с рук, потому что перечисления Java реализованы как полноценные классы с небольшим количеством синтаксического сахара, присыпанного сверху. Это опять же менее шаблонно, но это неочевидно, если только идиома вам не знакома. Мне также не нравится тот факт, что вы получаете различные функции enum, даже если они не имеют особого смысла для singleton: ord и values и т.д. (На самом деле существует более сложная симуляция, в которой Color extends Integer это будет работать с switch, но она настолько сложна, что еще более четко показывает, почему enum это лучшая идея.)

Потокобезопасность

Потокобезопасность является потенциальной проблемой только тогда, когда синглтоны создаются лениво, без блокировки.

public class SingletonClass {
private static SingletonClass INSTANCE;
private SingletonClass() {}
public SingletonClass getInstance() {
if (INSTANCE == null) INSTANCE = new SingletonClass();
return INSTANCE;
}

// all the methods and instance data for the class here
}

Если несколько потоков вызывают getInstance одновременно, а INSTANCE значение по-прежнему равно null, может быть создано любое количество экземпляров. Это плохо. Единственное решение - добавить synchronized доступ для защиты переменной INSTANCE.

Однако в static final приведенном выше коде этой проблемы нет. Он быстро создает экземпляр во время загрузки класса. Загрузка класса синхронизирована.

enum Синглтон фактически ленив, потому что он не инициализируется до первого использования. Инициализация Java также синхронизирована, поэтому несколько потоков не могут инициализировать более одного экземпляра INSTANCE. Вы получаете лениво инициализированный синглтон с очень небольшим количеством кода. Единственным недостатком является довольно неясный синтаксис. Вам нужно знать идиому или досконально понимать, как работают загрузка и инициализация класса, чтобы знать, что происходит.

Ответ 3

Помимо уже упомянутых вариантов использования, я часто нахожу перечисления полезными для реализации шаблона стратегии, следуя некоторым базовым рекомендациям по ООП:


  1. Наличие кода там, где находятся данные (то есть внутри самого перечисления - или часто внутри констант перечисления, которые могут переопределять методы).

  2. Реализация интерфейса (или более), чтобы не привязывать клиентский код к перечислению (который должен предоставлять только набор реализаций по умолчанию).

Самым простым примером может быть набор 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 new Integer(s1.length()).compareTo(s2.length());
}
};
}

Этот "шаблон" можно использовать в гораздо более сложных сценариях, широко используя все преимущества, которые прилагаются к перечислению: перебор экземпляров, полагаясь на их неявный порядок, получение экземпляра по его имени, статические методы, обеспечивающие нужный экземпляр для конкретных контекстов и т.д. И все же у вас все это скрыто за интерфейсом, поэтому ваш код будет работать с пользовательскими реализациями без изменений на случай, если вам понадобится что-то, чего нет среди "опций по умолчанию".

Я видел, как это успешно применялось для моделирования концепции временной детализации (ежедневной, еженедельной и т.д.), Где вся логика была инкапсулирована в enum (выбор правильной детализации для данного временного диапазона, конкретное поведение, привязанное к каждой детализации в виде постоянных методов и т.д.). И все же, Granularity как видится на уровне обслуживания, был просто интерфейсом.

Ответ 4

То, что не было рассмотрено ни в одном из других ответов, что делает перечисления особенно мощными, - это возможность иметь шаблонные методы. Методы могут быть частью базового перечисления и переопределяться каждым типом. И, благодаря поведению, привязанному к перечислению, это часто устраняет необходимость в конструкциях if-else или операторах switch, как демонстрирует это сообщение в блоге - where enum.method() выполняет то, что изначально должно было выполняться внутри условия. В том же примере также показано использование статического импорта с перечислениями, что позволяет создавать гораздо более чистый DSL-подобный код.

К некоторым другим интересным качествам относится тот факт, что перечисления обеспечивают реализацию для equals(), toString() и hashCode() и реализуют Serializable и Comparable.

Для полного изложения всего, что могут предложить перечисления, я настоятельно рекомендую книгу Брюса Эккеля "Размышления на Java, 4-е издание", в которой этой теме посвящена целая глава. Особенно наглядными являются примеры, включающие в качестве перечислений игру в камень, ножницы, бумагу (т. Е. RoShamBo).

java enums