Руководство по Java Enums

Руководство по Java Enums

1. обзор

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

The enum keyword was introduced in Java 5. Обозначает особый тип класса, который всегда расширяет классjava.lang.Enum. Официальную документацию по их использованию смотрите наdocumentation.

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

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

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. Сравнение типов перечислений с помощью оператора «==»

Поскольку типы перечислений гарантируют, что в JVM существует только один экземпляр констант, мы можем смело использовать оператор «==» для сравнения двух переменных, как показано в примере выше; Более того, оператор «==» обеспечивает безопасность во время компиляции и во время выполнения.

Давайте сначала посмотримat run-time safety в следующем фрагменте, где оператор «==» используется для сравнения статусов, аNullPointerException не будет выдан, если любое значение равноnull. И наоборот, если бы использовался метод equals, будет выданNullPointerException:

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

Что касаетсяcompile time safety, давайте посмотрим на другой пример, в котором перечисление другого типа сравнивается с использованием методаequals и определяется как истинное - потому что значения перечисления иgetStatus по совпадению совпадают, но логически сравнение должно быть ложным. Эту проблему можно избежать с помощью оператора «==».

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

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

4. Использование типов перечислений в операторах переключения

Типы перечислений также могут использоваться в операторахswitch:

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

5. Поля, методы и конструкторы в перечислениях

Вы можете определить конструкторы, методы и поля внутри перечислимых типов, которые делают его очень мощным.

Давайте расширим приведенный выше пример и реализуем переход от одного этапа пиццы к другому и посмотрим, как мы можем избавиться от операторов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.

Это очень эффективное и компактное представление конкретногоSet константEnum по сравнению с aHashSet, благодаря используемому внутреннемуBit Vector Representation. И он обеспечивает безопасную по типу альтернативу традиционным «битовым флагам» на основеint, позволяя нам писать краткий код, более читаемый и поддерживаемый.

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

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

В приведенном ниже фрагменте кода вы можете увидеть, какEnumSet используется для создания подмножества констант и его использования:

public class Pizza {

    private static EnumSet 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 getAllUndeliveredPizzas(List 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 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 undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
    assertTrue(undeliveredPzs.size() == 3);
}

6.2. EnumMap

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

EnumMap map;

Давайте быстро рассмотрим реальный пример, который показывает, как это можно использовать на практике:

public static EnumMap>
  groupPizzaByStatus(List pizzaList) {
    EnumMap> pzByStatus =
      new EnumMap>(PizzaStatus.class);

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

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

@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
    List 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> 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. Реализация шаблонов проектирования с использованием Enums

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. Шаблон стратегии

Традиционно шаблон «Стратегия» написан с использованием интерфейса, который реализуется различными классами.

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

Фрагмент кода ниже показывает, как реализовать шаблон «Стратегия»:

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 и Enums

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

public static List getAllUndeliveredPizzas(List input) {
    return input.stream().filter(
      (s) -> !deliveredPizzaStatuses.contains(s.getStatus()))
        .collect(Collectors.toList());
}
public static EnumMap>
  groupPizzaByStatus(List pzList) {
    EnumMap> 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-представления статусаPizzas:

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

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

10. Заключение

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

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