Перейти к содержанию

Руководство по перечислениям (enum) в Java

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

В Java 5 впервые появилось ключевое слово enum. Оно обозначает особый тип класса, который всегда расширяет класс java.lang.Enum. Для получения официальной документации по использованию можно перейти к документации.

Константы, определенные таким образом, делают код более читабельным, позволяют выполнять проверку во время компиляции, заранее документируют список допустимых значений и позволяют избежать неожиданного поведения из-за передачи недопустимых значений. Рассмотрим короткий и простой пример перечисления, определяющего статус заказа на пиццу; статус заказа может быть ORDERED, READY или DELIVERED:

public enum PizzaStatus {
    ORDERED,
    READY, 
    DELIVERED; 
}

Кроме того, перечисления поставляются со многими полезными методами, которые в противном случае пришлось бы написать, если бы использовали традиционные public static final константы.

Пользовательские методы Enum

Теперь, когда есть общее представление о том, что такое перечисления и как можно их использовать, поднимем предыдущий пример на следующий уровень, определив некоторые дополнительные методы API для перечисления:

public class Pizza {
    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED,
        READY,
        DELIVERED;
    }

    public boolean isDeliverable() {
        if (getStatus() == PizzaStatus.READY) {
            return true;
        }
        return false;
    }
    
    // Методы, которые устанавливают и получают переменную состояния.
}

Сравнение типов Enum с использованием оператора “==”

Поскольку типы enum гарантируют, что в JVM существует только один экземпляр констант, то можно безопасно использовать оператор “==” для сравнения двух переменных, как сделали в приведенном выше примере. Кроме того, оператор “==” обеспечивает безопасность во время компиляции и во время выполнения.

Во-первых, рассмотрим безопасность во время выполнения в следующем фрагменте, где будем использовать оператор “==” для сравнения статусов. Любое значение может быть нулевым, и мы не получим исключение NullPointerException. И наоборот, если используем метод equals, то получим исключение NullPointerException:

if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); 
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); 

Что касается безопасности во время компиляции, рассмотрим пример, в котором определим, что перечисление другого типа равно, сравнивая его с помощью метода equals. Это связано с тем, что значения перечисления и метода getStatus совпадают; однако по логике сравнение должно быть ложным. Мы избегаем этой проблемы, используя оператор “==”.

Компилятор пометит сравнение как ошибку несовместимости:

if(testPz.getStatus().equals(TestColor.GREEN));
if(testPz.getStatus() == TestColor.GREEN);

Использование типов Enum в операторах Switch

Можно использовать типы enum в операторах switch:

public int getDeliveryTimeInDays() {
    switch (status) {
        case ORDERED: return 5;
        case READY: return 2;
        case DELIVERED: return 0;
    }
    return 0;
}

Поля, методы и конструкторы в Enum

Можно определять конструкторы, методы и поля внутри типов enum, что делает их очень мощными. Расширим приведенный выше пример, реализовав переход от одного этапа заказа пиццы к другому. Мы увидим, как можно избавиться от операторов if и switch, которые использовались ранее:

public class Pizza {

    private PizzaStatus status;
    public enum PizzaStatus {
        ORDERED (5){
            @Override
            public boolean isOrdered() {
                return true;
            }
        },
        READY (2){
            @Override
            public boolean isReady() {
                return true;
            }
        },
        DELIVERED (0){
            @Override
            public boolean isDelivered() {
                return true;
            }
        };

        private int timeToDelivery;

        public boolean isOrdered() {return false;}

        public boolean isReady() {return false;}

        public boolean isDelivered(){return false;}

        public int getTimeToDelivery() {
            return timeToDelivery;
        }

        PizzaStatus (int timeToDelivery) {
            this.timeToDelivery = timeToDelivery;
        }
    }

    public boolean isDeliverable() {
        return this.status.isReady();
    }

    public void printTimeToDeliver() {
        System.out.println("Время доставки " + 
          this.getStatus().getTimeToDelivery());
    }
    
    // Методы, которые устанавливают и получают переменную состояния.
}

Фрагмент теста ниже демонстрирует, как это работает:

@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
    Pizza testPz = new Pizza();
    testPz.setStatus(Pizza.PizzaStatus.READY);
    assertTrue(testPz.isDeliverable());
}

EnumSet и EnumMap

EnumSet

EnumSet – это специализированная реализация Set, предназначенная для использования с типами Enum.

По сравнению с HashSet это очень эффективное и компактное представление конкретного набора констант Enum благодаря используемому внутреннему представлению битового флага. Он также предоставляет типобезопасную альтернативу традиционным «битовым флагам» на основе int, позволяя писать лаконичный код, более читаемый и удобный для сопровождения.

EnumSet – это абстрактный класс, имеющий две реализации, RegularEnumSet и JumboEnumSet, одна из которых выбирается в зависимости от количества констант в перечислении на момент создания экземпляра.

Поэтому рекомендуется использовать этот набор всякий раз, когда хотим работать с набором констант перечисления в большинстве сценариев (например, подмножество, добавление, удаление и массовые операции, такие как containsAll и removeAll), и использовать Enum.values(), если хотим перебрать все возможные константы. В приведенном ниже фрагменте кода показано, как использовать EnumSet для создания подмножества констант:

public class Pizza {

    private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =
      EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);

    private PizzaStatus status;

    public enum PizzaStatus {
        ...
    }

    public boolean isDeliverable() {
        return this.status.isReady();
    }

    public void printTimeToDeliver() {
        System.out.println("Время доставки " + 
          this.getStatus().getTimeToDelivery() + " дней");
    }

    public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
        return input.stream().filter(
          (s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
            .collect(Collectors.toList());
    }

    public void deliver() { 
        if (isDeliverable()) { 
            PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
              .deliver(this); 
            this.setStatus(PizzaStatus.DELIVERED); 
        } 
    }
    
    // Методы, которые устанавливают и получают переменную состояния.
}

Выполнение следующего теста демонстрирует мощь реализации EnumSet интерфейса Set:

@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);

    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);

    List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList); 
    assertTrue(undeliveredPzs.size() == 3); 
}

EnumMap

EnumMap – это специализированная реализация Map, предназначенная для использования с константами перечисления в качестве ключей. По сравнению со своим аналогом HashMap это эффективная и компактная реализация, которая внутренне представлена в виде массива:

EnumMap<Pizza.PizzaStatus, Pizza> map;

Рассмотрим пример того, как можно использовать это на практике:

public static EnumMap<PizzaStatus, List<Pizza>> 
  groupPizzaByStatus(List<Pizza> pizzaList) {
    EnumMap<PizzaStatus, List<Pizza>> pzByStatus = 
      new EnumMap<PizzaStatus, List<Pizza>>(PizzaStatus.class);
    
    for (Pizza pz : pizzaList) {
        PizzaStatus status = pz.getStatus();
        if (pzByStatus.containsKey(status)) {
            pzByStatus.get(status).add(pz);
        } else {
            List<Pizza> newPzList = new ArrayList<Pizza>();
            newPzList.add(pz);
            pzByStatus.put(status, newPzList);
        }
    }
    return pzByStatus;
}

Выполнение следующего теста демонстрирует мощь реализации EnumMap интерфейса Map:

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
    List<Pizza> pzList = new ArrayList<>();
    Pizza pz1 = new Pizza();
    pz1.setStatus(Pizza.PizzaStatus.DELIVERED);

    Pizza pz2 = new Pizza();
    pz2.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz3 = new Pizza();
    pz3.setStatus(Pizza.PizzaStatus.ORDERED);

    Pizza pz4 = new Pizza();
    pz4.setStatus(Pizza.PizzaStatus.READY);

    pzList.add(pz1);
    pzList.add(pz2);
    pzList.add(pz3);
    pzList.add(pz4);

    EnumMap<Pizza.PizzaStatus,List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
    assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
    assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
    assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}

Реализация паттернов проектирования с использованием перечислений

Паттерн Singleton

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

Кроме того, поскольку класс enum реализует интерфейс Serializable под капотом, JVM гарантирует, что класс будет одноэлементным. Это отличается от обычной реализации, где необходимо гарантировать, что во время десериализации не будут созданы новые экземпляры.

В приведенном ниже фрагменте кода видим, как можно реализовать одноэлементный паттерн:

public enum PizzaDeliverySystemConfiguration {
    INSTANCE;
    PizzaDeliverySystemConfiguration() {
        // Initialization configuration which involves
        // overriding defaults like delivery strategy
    }

    private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;

    public static PizzaDeliverySystemConfiguration getInstance() {
        return INSTANCE;
    }

    public PizzaDeliveryStrategy getDeliveryStrategy() {
        return deliveryStrategy;
    }
}

Паттерн Strategy

Обычно паттерн Strategy записывается с интерфейсом, реализуемым разными классами.

Добавление новой стратегии означает добавление нового класса реализации. С перечислениями можно добиться этого с меньшими усилиями, а добавление новой реализации означает простое определение другого экземпляра с некоторой реализацией. Фрагмент кода ниже показывает, как реализовать паттерн Strategy:

public enum PizzaDeliveryStrategy {
    EXPRESS {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Пицца будет доставлена в экспресс-режиме.");
        }
    },
    NORMAL {
        @Override
        public void deliver(Pizza pz) {
            System.out.println("Пицца будет доставлена в обычном режиме.");
        }
    };

    public abstract void deliver(Pizza pz);
}

Затем добавляем следующий метод в класс Pizza:

public void deliver() {
    if (isDeliverable()) {
        PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
          .deliver(this);
        this.setStatus(PizzaStatus.DELIVERED);
    }
}
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
    Pizza pz = new Pizza();
    pz.setStatus(Pizza.PizzaStatus.READY);
    pz.deliver();
    assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}

Java 8 и перечисления

Можно переписать класс Pizza на Java 8 и посмотреть, как методы getAllUndeliveredPizzas() и groupPizzaByStatus() становятся такими лаконичными с использованием лямбда-выражений и Stream API:

public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
    return input.stream().filter(
      (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
        .collect(Collectors.toList());
}
public static EnumMap<PizzaStatus, List<Pizza>> 
  groupPizzaByStatus(List<Pizza> pzList) {
    EnumMap<PizzaStatus, List<Pizza>> map = pzList.stream().collect(
      Collectors.groupingBy(Pizza::getStatus,
      () -> new EnumMap<>(PizzaStatus.class), Collectors.toList()));
    return map;
}

JSON-представление Enum

Используя библиотеки Jackson, можно иметь JSON-представление типов enum, как если бы они были POJO. В приведенном ниже фрагменте кода увидим, как можно использовать аннотации Jackson для того же самого:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
    ORDERED (5){
        @Override
        public boolean isOrdered() {
            return true;
        }
    },
    READY (2){
        @Override
        public boolean isReady() {
            return true;
        }
    },
    DELIVERED (0){
        @Override
        public boolean isDelivered() {
            return true;
        }
    };

    private int timeToDelivery;

    public boolean isOrdered() {return false;}

    public boolean isReady() {return false;}

    public boolean isDelivered(){return false;}

    @JsonProperty("timeToDelivery")
    public int getTimeToDelivery() {
        return timeToDelivery;
    }

    private PizzaStatus (int timeToDelivery) {
        this.timeToDelivery = timeToDelivery;
    }
}

Можно использовать Pizza и PizzaStatus следующим образом:

Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));

Это создаст следующее JSON-представление статуса пиццы:

{
  "status" : {
    "timeToDelivery" : 2,
    "ready" : true,
    "ordered" : false,
    "delivered" : false
  },
  "deliverable" : true
}

Для получения дополнительной информации о сериализации/десериализации JSON (включая настройку) типов перечислений можно обратиться к Jackson – сериализовать перечисления как объекты JSON.

Заключение

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

Фрагменты кода из этой статьи можно найти в репозитории Github.

Оригинал