Перейти к содержимому
Теория · Интерфейсы, enum и record · урок 5 из 8

Перечисления enum

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

Как хранить такую карту в программе? Можно строкой: String card = "king";. Но тогда компилятор пропустит и опечатку "kign", и карту "joker", которой в колоде нет. Число вроде int card = 2; не лучше: по коду не понять, что значит 2, а card = 9 тоже скомпилируется. Нужен тип, у которого есть только перечисленные значения:

JAVA
public class Main {
    public static void main(String[] args) {
        Card c = Card.KING;
        System.out.println(c);              // KING
        System.out.println(c.ordinal());    // 2
        System.out.println(c == Card.KING); // true
        for (Card x : Card.values()) {
            System.out.print(x + " "); // JACK QUEEN KING ACE
        }
    }
}

enum Card { JACK, QUEEN, KING, ACE }

Давайте разберём программу по шагам:

  1. enum Card { … } после Main объявляет тип Card ровно с четырьмя значениями. Их имена пишут заглавными буквами через запятую, как имена констант.
  2. Card c = Card.KING; — к значению обращаются через имя типа и точку.
  3. println(c) печатает имя значения: KING.
  4. c.ordinal() возвращает номер значения в объявлении, считая с нуля: у JACK номер 0, у QUEEN — 1, у KING — 2.
  5. c == Card.KING даёт true.
  6. Цикл for-each перебирает массив Card.values() и печатает все карты по порядку.

Что такое enum

Перечисление — тип, все значения которого перечислены в его объявлении. Объявляют его словом enum, а значения называют константами перечисления. Каждая константа — объект типа Card, и Java создаёт его ровно один раз.

Что из этого следует:

  • новых значений не бывает: на new Card() компилятор ответит enum classes may not be instantiated;
  • переменной типа Card нельзя присвоить строку или число, только одну из четырёх констант или null;
  • две переменные со значением Card.KING всегда ведут к одному объекту.

На собеседовании спрашивают, можно ли сравнивать перечисления через ==. Можно: каждая константа существует в одном экземпляре, и == даёт тот же ответ, что и equals.

Здесь картинка с колодой неточна. Колод можно напечатать сколько угодно, и в каждой будет свой король. Константа Card.KING в программе одна на всех: её не копируют и не печатают заново.

name, ordinal и values

У каждого перечисления есть готовые методы:

  • name() возвращает имя константы, как оно написано в коде: "KING"; println печатает то же имя;
  • ordinal() возвращает номер константы в объявлении, считая с нуля;
  • Card.values() возвращает массив всех констант в порядке объявления.

Номер зависит только от порядка в объявлении: переставьте константы, и номера изменятся. Поэтому ordinal() не берут как постоянный код значения — например, чтобы сохранить его в файл.

enum в switch

Перечисление удобно разбирать в switch. В метках case константы пишут без имени типа. Посчитаем очки карты:

JAVA
int points = switch (c) {
    case JACK -> 2;
    case QUEEN -> 3;
    case KING -> 4;
    case ACE -> 11;
};
System.out.println(points); // 4

Как вы уже знаете, switch-выражение должно дать значение для любого входа. Здесь перечислены все четыре константы, поэтому default не нужен. Если одну забыть, компилятор ответит the switch expression does not cover all possible input values. Так он подскажет и тогда, когда в колоду добавят новую карту.

Поле и конструктор

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

JAVA
public class Main {
    public static void main(String[] args) {
        System.out.println(Card.ACE.points); // 11
    }
}

enum Card {
    JACK(2), QUEEN(3), KING(4), ACE(11);

    final int points;

    Card(int points) {
        this.points = points;
    }
}

Разберём объявление:

  1. JACK(2) — константа с аргументом: для неё Java вызывает конструктор Card(2).
  2. После последней константы стоит точка с запятой, а дальше идут поле и конструктор.
  3. final int points; — у каждой карты своё число очков, и после конструктора оно не меняется.
  4. Card.ACE.points даёт 11.

Конструктор перечисления вызывает только сама Java, по одному разу для каждой константы. Теперь новая карта приходит в колоду вместе со своими очками, и забыть их не получится.

Частая ошибка

Константу сравнили со строкой:

JAVA
Card c = Card.KING;
if (c.equals("KING")) { // ✗
    System.out.println("king");
}

Программа ничего не печатает и не сообщает об ошибке. c — объект типа Card, а "KING" — строка, и equals для объектов разных типов даёт false. Запись c == "KING" компилятор бы отверг с ошибкой incomparable types: Card and String, а вызов equals пропускает без ошибки. Сравнивайте с константой: c == Card.KING. Если на входе строка, сравнивайте с ней имя: c.name().equals("KING").

Главное

  • enum объявляет тип с заранее перечисленными значениями; новых через new не создать.
  • Каждая константа существует в одном экземпляре, поэтому перечисления сравнивают через ==.
  • name() и println дают имя константы, ordinal() — номер с нуля, values() — массив всех констант.
  • switch-выражение по перечислению без default должно перечислить все константы; поля и конструктор задают данные каждой константы.