Как использовать enum в Java

Джефф Фризен, JavaWorld | 12 ноября 2020

Использование в Java в качестве перечислений традиционных примитивных типов проблематично. Java 5 предоставила нам лучшую альтернативу в виде типизированных перечислений (enum). В этой статье я познакомлю вас с типизированными перечислениями, покажу вам, как объявить типизированное перечисление и использовать его в операторе switch, а также научу настраивать enum путем добавления данных и методов. И в конце статьи мы изучим класс java.lang.Enum<E extends Enum<E>>.

От перечислимых типов к типизированным перечислениям

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

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

static final int DIR_NORTH = 0;
static final int DIR_WEST = 1;
static final int DIR_EAST = 2;
static final int DIR_SOUTH = 3;

У такого подхода есть несколько проблем:

  • Отсутствует типобезопасность: поскольку константа перечислимого типа является просто целым числом, любое целое число может быть указано там, где требуется константа. Кроме того, над этими константами можно выполнять сложение, вычитание и другие математические операции, например (DIR_NORTH + DIR_EAST) / DIR_SOUTH), что бессмысленно.
  • Отсутствует пространство имен: константы перечисляемого типа должны иметь префикс какого-либо (надеюсь) уникального идентификатора (например, DIR_), чтобы предотвратить конфликты с константами другого перечислимого типа.
  • Возможность сломать код: поскольку константы перечислимого типа компилируются в class-файлы, где хранятся их реальные значения (в пулах констант), изменение значения константы требует, чтобы эти class-файлы и те class-файлы приложений, которые от них зависят, были пересобраны. В противном случае поведение программы может стать непредсказуемым.
  • Недостаток информации: при печати константы выводится ее целочисленное значение. Этот вывод ничего не говорит вам о том, что представляет собой целочисленное значение. Он даже не идентифицирует перечислимый тип, которому принадлежит константа.

Вы можете избежать проблем «отсутствия типобезопасности» и «недостатка информации», используя java.lang.String константы. Например, вы можете указать static final String DIR_NORTH = "NORTH";. Хотя значение константы более информативно, String константы по-прежнему страдают от «отсутствия пространства имен» и проблем с возможностью сломать код. Кроме того, вы не сможете сравнивать значение строк операторами == и != (которые в случае с объектами сравнивают ссылки).

Эти проблемы заставили разработчиков изобрести альтернативу на основе классов, известную как Typesafe Enum . Этот шаблон широко описывался и подвергался критике . Джошуа Блох представил шаблон в пункте 21 своего Руководства по эффективному языку программирования Java (Addison-Wesley, 2001) и отметил, что в нем есть некоторые проблемы; а именно, что неудобно агрегировать константы Typesafe Enum в наборы, и что константы в качестве перечислений не могут использоваться в операторе switch.

Рассмотрим следующий пример шаблона Typesafe Enum. Класс Suit показывает как вы могли бы использовать альтернативный вариант, основанный на классах для перечисления карточных мастей (трефы, бубны, червы, пика):

public final class Suit // Should not be able to subclass Suit.
{
   public static final Suit CLUBS = new Suit();
   public static final Suit DIAMONDS = new Suit();
   public static final Suit HEARTS = new Suit();
   public static final Suit SPADES = new Suit();
   private Suit() {} // Should not be able to introduce additional constants.
}

Чтобы использовать этот класс, вы должны ввести переменную типа Suit и присвоить ее одной из Suit-констант следующим образом:

Suit suit = Suit.DIAMONDS;

Затем вы можете проверить suit в таком операторе switch:

switch (suit)
{
   case Suit.CLUBS   : System.out.println("clubs"); break;
   case Suit.DIAMONDS: System.out.println("diamonds"); break;
   case Suit.HEARTS  : System.out.println("hearts"); break;
   case Suit.SPADES  : System.out.println("spades");
}

Однако, когда компилятор Java обнаруживает Suit.CLUBS, он сообщает об ошибке, в которой говорится, что требуется константное выражение. Вы можете попытаться решить проблему следующим образом:

switch (suit)
{
   case CLUBS   : System.out.println("clubs"); break;
   case DIAMONDS: System.out.println("diamonds"); break;
   case HEARTS  : System.out.println("hearts"); break;
   case SPADES  : System.out.println("spades");
}

Но в таком случае, когда компилятор обнаруживает CLUBS, он сообщит об ошибке, в которой говорится, что ему не удалось найти символ. И даже если вы поместили Suit в пакет, импортировали пакет и статически импортировали эти константы, компилятор будет жаловаться на то, что он не может преобразовать Suit в int при обнаружении suit в switch(suit). А в каждом case компилятор сообщит, что требуется константное выражение.

Java не поддерживает шаблон Typesafe Enum с оператором switch. Зато поддерживает enum, который обладает всеми преимуществами этого шаблона и устраняет его проблемы, и enum уже поддерживается в switch.

Объявление enum и использование его в операторе switch

Простое объявление enum в коде Java выглядит так же, как его аналоги в языках C, C ++ и C #:

enum Direction { NORTH, WEST, EAST, SOUTH }

Здесь используется ключевое слово enum для объявления Direction в качестве типизированного перечисления (особый тип класса), в которое могут быть добавлены произвольные методы и реализованы произвольные интерфейсы. Константы enum NORTH, WEST, EAST и SOUTH являются экземплярами анонимного класса — потомка Direction.

Direction и другие типизированные перечисления являются потомками класса Enum<E extends Enum<E>> и наследуют различные методы, в том числе values(), toString() и compareTo() из этого класса. Мы рассмотрим класс Enum позже в этой статье.

Листинг 1 объявляет вышеупомянутое перечисление и использует его в конструкции switch. Он также показывает, как сравнить две константы enum, чтобы определить их порядок в объявлении.

Листинг 1: TEDemo.java(версия 1)
public class TEDemo
{
   enum Direction { NORTH, WEST, EAST, SOUTH }
   public static void main(String[] args)
   {
      for (int i = 0; i < Direction.values().length; i++)
      {
         Direction d = Direction.values()[i];
         System.out.println(d);
         switch (d)
         {
            case NORTH: System.out.println("Move north"); break;
            case WEST : System.out.println("Move west"); break;
            case EAST : System.out.println("Move east"); break;
            case SOUTH: System.out.println("Move south"); break;
            default   : assert false: "unknown direction";
         }
      }
      System.out.println(Direction.NORTH.compareTo(Direction.SOUTH));
   }
}

В листинге 1 объявляется enum Direction и в цикле осуществляется проход по массиву всех его констант-объектов, который возвращается методом values(). Для каждого значения d оператор switch (расширенный для поддержки enum) выбирает соответствующую константу и выводит сообщение. (При этом ставить префикс перед константой enum, например, NORTH, не требуется.) Наконец, в листинге 1 выполняется сравнение:

Direction.NORTH.compareTo(Direction.SOUTH)

чтобы определить что объявлено раньше NORTH или SOUTH.

Скомпилируйте исходный код следующим образом:

javac TEDemo.java

Запустите скомпилированное приложение следующим образом:

java TEDemo

Вы должны увидеть следующий результат:

NORTH
Move north
WEST
Move west
EAST
Move east
SOUTH
Move south
-3

Выходные данные показывают, что унаследованный метод toString() возвращает имя константы перечисления, и что NORTH объявлен раньше чем SOUTH.

Добавление данных и поведения в типизированное перечисление

Вы можете добавлять данные (в виде полей) и поведение (в виде методов) в типизированное перечисление. Например, предположим, что вам нужно ввести enum для канадских монет, и этот класс должен предоставить средства для возврата количества пятицентовиков, десятицентовиков, двадцатипятицентовиков или долларов, содержащихся в произвольном количестве центов. В листинге 2 показано, как выполнить эту задачу.

Листинг 2: TEDemo.java (версия 2)
enum Coin
{
   NICKEL(5),   // constants must appear first
   DIME(10),
   QUARTER(25),
   DOLLAR(100); // the semicolon is required
   private final int valueInPennies;
   Coin(int valueInPennies)
   {
      this.valueInPennies = valueInPennies;
   }
   int toCoins(int pennies)
   {
      return pennies / valueInPennies;
   }
}
public class TEDemo
{
   public static void main(String[] args)
   {
      if (args.length != 1)
      {
          System.err.println("usage: java TEDemo amountInPennies");
          return;
      }
      int pennies = Integer.parseInt(args[0]);
      for (int i = 0; i < Coin.values().length; i++)
           System.out.println(pennies + " pennies contains " +
                              Coin.values()[i].toCoins(pennies) + " " +
                              Coin.values()[i].toString().toLowerCase() + "s");
   }
}

В листинге 2 сначала объявляется enum Coin. Список параметризованных констант определяет четыре типа монет. Аргумент, передаваемый каждой константе, определяет количество центов в данной монете.

Аргумент, переданный каждой константе, фактически передается конструктору Coin(int valueInPennies), который сохраняет аргумент в поле экземпляра valuesInPennies. Доступ к этой переменной осуществляется из метода экземпляра toCoins(). На неё делится переданное в метод toCoins() количество центов, после чего возвращается сколько монет, определённых данной Coin константой, содержится в переданном количестве центов.

Теперь вы знаете, что можете объявлять поля, конструкторы и методы экземпляра в типизированном перечислении (enum). А значит, enum — это, по сути, особый вид Java-класса.

Метод main() класса TEDemo сначала проверяет, указан ли единственный аргумент командной строки. Этот аргумент преобразуется в целое число путем вызова метода parseInt() класса java.lang.Integer, который преобразует значение строкового аргумента в целое число (или выдает исключение при обнаружении недопустимого ввода). Я расскажу больше об Integer и других классах-обёртках в будущей статье Java 101.

Идём дальше. В методе main() перебираются Coin константы. Поскольку эти константы хранятся в массиве Coin[], мы получаем длину этого массива через Coin.values().length. В каждой итерации цикла мы получаем доступ к Coin константе через Coin.values()[i]. Для полученной константы вызывается toCoins() и toString(), что еще раз доказывает, что Coin это особый тип класса.

Скомпилируйте исходный код следующим образом:

javac TEDemo.java

Запустите скомпилированное приложение следующим образом:

Java TEDemo 198

Вы должны увидеть следующий результат:

198 pennies contains 39 nickels
198 pennies contains 19 dimes
198 pennies contains 7 quarters
198 pennies contains 1 dollars

Компилятор Java считает enum синтаксическим сахаром. При обнаружении объявления типизированного перечисления он генерирует класс, имя которого указано в объявлении. Этот класс является подклассом абстрактного Enum<E extends Enum<E>> класса, который служит базовым классом для всех типов перечислений.

Список формальных параметров класса Enum выглядит ужасно, но смысл не так уж и сложно понять. Например, Coin extends Enum<Coin> интерпретируется следующим образом:

  • Любой подкласс Enum должен предоставлять фактический аргумент типа Enum. Например, для Coin это будет Enum<Coin>.
  • Фактический аргумент типа должен быть подклассом Enum. Например, для Coin это подкласс Enum.
  • Подкласс Enum (например, Coin) должен следовать идиоме, которая предоставляет собственное имя (Coin) в качестве фактического аргумента типа.

Исследуйте документацию Java по Enum, и вы обнаружите, что в нём переопределены методы java.lang.Object: clone(), equals(), finalize(), hashCode() и toString(). Все эти переопределённые методы, за исключением toString(), объявлены как final, так что их нельзя переопределить в подклассе:

  • clone() переопределяется, чтобы предотвратить клонирование констант, чтобы было не больше одной копии константы; в противном случае константы нельзя было бы сравнивать с помощью == и !=.
  • equals() переопределяется для сравнения констант по ссылкам на них. Константы с одинаковыми названиями (==) должны иметь одинаковое содержимое (equals()), а разные названия подразумевают разное содержимое.
  • finalize() переопределяется с целью запрета удаления объектов констант.
  • hashCode() переопределяется за компанию с equals().
  • toString() переопределяется, чтобы вернуть имя константы.

Enum также предоставляет свои собственные методы. В том числе final compareTo() (Enum реализует интерфейс java.lang.Comparable<T>), getDeclaringClass(), name() и ordinal():

  • compareTo() сравнивает текущую константу с константой, переданной в качестве аргумента, чтобы увидеть, какая константа предшествует другой константе в перечислении, и возвращает значение, указывающее порядковый номер. Этот метод позволяет отсортировать массив несортированных констант.
  • getDeclaringClass() возвращает объект типа java.lang.Class, соответствующий enum текущей константы. Например, в enum Coin { PENNY, NICKEL, DIME, QUARTER} при вызове Coin.PENNY.getDeclaringClass() будет возвращён объект типа Class для Coin (я расскажу об этом в будущей статье Java 101)
  • name() возвращает имя константы. Если toString() не переопределен нами для своих целей, он также вернёт имя константы.
  • ordinal() возвращает целое число — порядковый номер начиная с нуля, которое определяет позицию константы в enum. Это целое число используется в compareTo(). Например, в предыдущем примере выражение Direction.NORTH.compareTo(Direction.SOUTH) вернуло -3, потому что NORTH имеет порядковый номер 0, а SOUTH имеет порядковый номер 3 и 0–3 (именно так здесь работает compareTo()) равно -3.

Enum также предоставляет метод public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) для возврата константы из указанного перечисления типов с указанным именем:

  • enumType идентифицирует Class объекта перечисления, из которого следует вернуть константу.
  • name определяет имя возвращаемой константы.

Например, Coin penny = Enum.valueOf(Coin.class, "PENNY"); присваивает переменной penny значение Coin константы с именем PENNY.

Наконец, Enum документация по Java не включает values() метод, потому что компилятор создает этот метод при создании файла класса enum.

Вам следует выработать привычку использовать типизированные перечисления (enum) вместо традиционных перечисляемых типов. Кроме того, возьмите за привычку использовать лямбды вместо анонимных внутренних классов. Про лямбды я расскажу в следующей статье Java 101.

Перевод Академии Progwards

Оригинал