1. Обзор
В этом руководстве мы узнаем, что такое перечисления Java, какие проблемы они решают и как некоторые из их шаблонов проектирования можно использовать на практике.
В Java 5 впервые появилось ключевое слово enum
. Он обозначает особый тип класса, который всегда расширяет класс java.lang.Enum
. Официальную документацию по использованию мы можем найти в документации .
Константы, определенные таким образом, делают код более читабельным, позволяют выполнять проверку во время компиляции, заранее документируют список допустимых значений и позволяют избежать неожиданного поведения из-за передачи недопустимых значений.
Вот быстрый и простой пример перечисления, определяющего статус заказа на пиццу; статус заказа может быть ЗАКАЗАН
, ГОТОВ
или ДОСТАВЛЕН
:
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
Кроме того, перечисления поставляются со многими полезными методами, которые в противном случае нам пришлось бы написать, если бы мы использовали традиционные общедоступные статические конечные константы.
2. Пользовательские методы перечисления
Теперь, когда у нас есть общее представление о том, что такое перечисления и как мы можем их использовать, мы поднимем наш предыдущий пример на следующий уровень, определив некоторые дополнительные методы API для перечисления:
public class Pizza {
private PizzaStatus status;
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
public boolean isDeliverable() {
if (getStatus() == PizzaStatus.READY) {
return true;
}
return false;
}
// Methods that set and get the status variable.
}
3. Сравнение типов 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);
4. Использование типов Enum в операторах Switch
Мы также можем использовать типы enum в операторах switch
:
public int getDeliveryTimeInDays() {
switch (status) {
case ORDERED: return 5;
case READY: return 2;
case DELIVERED: return 0;
}
return 0;
}
5. Поля, методы и конструкторы в Enums
Мы можем определять конструкторы, методы и поля внутри перечисляемых типов, что делает их очень мощными.
Далее давайте расширим приведенный выше пример, реализовав переход от одного этапа заказа пиццы к другому. Мы увидим, как мы можем избавиться от операторов 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("Time to delivery is " +
this.getStatus().getTimeToDelivery());
}
// Methods that set and get the status variable.
}
Фрагмент теста ниже демонстрирует, как это работает:
@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
Pizza testPz = new Pizza();
testPz.setStatus(Pizza.PizzaStatus.READY);
assertTrue(testPz.isDeliverable());
}
6. EnumSet
и EnumMap
6.1. 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("Time to delivery is " +
this.getStatus().getTimeToDelivery() + " days");
}
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);
}
}
// Methods that set and get the status variable.
}
Выполнение следующего теста демонстрирует мощь реализации 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);
}
6.2. ЭнумКарта
EnumMap
— это специализированная реализация карты
, предназначенная для использования с константами перечисления в качестве ключей. По сравнению со своим аналогом 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);
}
7. Реализация шаблонов проектирования с использованием перечислений
7.1. Синглтон шаблон
Обычно реализация класса с использованием шаблона 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;
}
}
7.2. Паттерн стратегии
Обычно шаблон Strategy записывается с интерфейсом, реализуемым разными классами.
Добавление новой стратегии означает добавление нового класса реализации. С перечислениями мы можем добиться этого с меньшими усилиями, а добавление новой реализации означает простое определение другого экземпляра с некоторой реализацией.
Фрагмент кода ниже показывает, как реализовать шаблон стратегии:
public enum PizzaDeliveryStrategy {
EXPRESS {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in express mode");
}
},
NORMAL {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in normal mode");
}
};
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);
}
8. 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;
}
9. JSON-представление Enum
Используя библиотеки Джексона, можно иметь JSON-представление типов перечисления, как если бы они были POJO. В приведенном ниже фрагменте кода мы увидим, как мы можем использовать аннотации Джексона для того же самого:
@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 — Serialize Enums as JSON Objects .
10. Заключение
В этой статье мы рассмотрели перечисление Java, от основ языка до более продвинутых и интересных вариантов использования в реальном мире.
Фрагменты кода из этой статьи можно найти в репозитории Github .