Давайте посмотрим на мощь и красоту того, что может предоставить Java Enums…

Я также создал видео, описывающее те же темы, на основе этой статьи, так что вы можете продолжить, объединив видео и этот пост.

Что такое перечисление?

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

Например:

enum Level {
  INFO,
  WARNING,
  ERROR
}

Проекты, использующие перечисления

Некоторые из моих «любимых проектов» широко используют перечисления Java, и, работая над ними, я понял, что не всем понятно, что они действительно мощные и могут содержать гораздо больше, чем просто список фиксированных значений.

Давайте рассмотрим несколько таких вариантов использования.

Платы Raspberry Pi в базе данных Enum

Для проекта Pi4J требовалась библиотека, содержащая некоторую информацию о различных платах Raspberry Pi и о том, какие протоколы они поддерживают на своих контактах GPIO (ввод/вывод общего назначения). Поскольку эта информация должна быть доступна внутри кода, на досках, работающих без подключения к Интернету, API не был правильным решением.

Поэтому было принято решение сгенерировать библиотеку Java, содержащую всю эту информацию, используя список перечислений: github.com/Pi4J/pi4j-board-info.

Поверх этой библиотеки создается веб-сайт и API по адресу api.pi4j.com. В результате базу данных теперь можно использовать в автономном и онлайн-режиме.

Парсинг LottieFiles

В другом проекте Lottie4J я пытаюсь разобрать анимацию в формате JSON в объекты Java для создания проигрывателя JavaFX. Чтобы упростить понимание формата LottieFiles в рамках этого Java-проекта, перечисления используются для различных определений, которые вы можете видеть в проекте GitHub.

Категории видео для 4drums.media

Этот проект вдохновлен моим 12-летним сыном, который хочет собирать и делиться барабанными видео и надеется, что сможет создать вокруг этого сообщество. Приятно иметь возможность использовать свои «профессиональные знания» для быстрого создания такого проекта благодаря Spring и Vaadin, но работа еще не завершена, так как мой сын создал список TODO с идеями из 100 оставшихся пунктов…

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

public enum VideoCategory {
    SPECTACULAR("la-dragon", "category.video.spectacular", "#CD0000", "#FFFFFF"),
    TUTORIAL("la-drum", "category.video.tutorial", " #0000EE", "#FFFFFF"),
    I_MADE_THIS("la-portrait", "category.video.selfmade", " #A2CD5A", "#000000"),
    ...;

    private final String icon;
    private final String translationId;
    private final String colorBg;
    private final String colorText;

    VideoCategory(String icon, String translationId, String colorBg, String colorText) {
        this.icon = icon;
        this.translationId = translationId;
        this.colorBg = colorBg;
        this.colorText = colorText;
    }

    public String getIcon() {
        return icon;
    }
    ...
}

Примеры использования перечисления

Давайте рассмотрим несколько примеров, шаг за шагом. Полный код доступен на GitHub как EnumExtended.java и может быть выполнен с помощью JBang! с jbang EnumExtended.java.

Базовая функциональность

Основным примером использования Enum является следующее:

enum Level {
  INFO,
  WARNING,
  ERROR
}

System.out.println("Error level: " + Level.WARNING);

Выход:

Error level: WARNING

Перечисления были введены для замены использования констант int, как это видно в других языках, которые выглядели бы так в Java:

class MyProgram {
   public final static int LEVEL_INFO = 1;
   public final static int LEVEL_WARNING = 2;
   public final static int LEVEL_ERROR = 3;
   ...
}

Но enum может сделать намного больше! Давайте посмотрим на некоторые примеры…

Использование перечислений в Switch

Используя перечисления, код switch - case может быть написан очень чисто.

public int getLevelValue(Level level) {
    switch (level) {
        case INFO: 
            return 1;
        case WARNING: 
            return 2;
        case ERROR: 
            return 3;
    }
    return 0;
}

Но, как мы увидим дальше, этот код все еще можно значительно улучшить, расширив перечисление Level…

Используйте Enum вместо Boolean

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

class Customer {
    String name;
    boolean active;
    ...
}

Но чуть позже ваш вариант использования изменится, и клиенты также могут быть заблокированы, потому что они перестали платить по счетам. Не могли бы вы добавить еще boolean suspended? Это может привести к большому количеству изменений в вашей программе, которых можно избежать, используя перечисление для состояния клиента, например:

enum CustomerState {
    ACTIVE,
    INACTIVE,
    SUSPENDED
}

class Customer {
    String name;
    CustomerState state;
    ...
}

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

Для меня истинная сила перечисления заключается в том, что оно может содержать много информации и предоставлять методы для использования этих значений.

В следующем примере перечисление Level, которое мы использовали ранее, расширено значением серьезности, описанием метки и цветовым кодом.

enum Level {
    INFO(1, "Informative message", 0x00aa00),
    WARNING(2, "Warning message", 0xFFA500),
    ERROR(3, "Error message", 0xA30000);

    private final int severity;
    private final String label;
    private final int color;

    private Level(int severity, String label, int color) {
        this.severity = severity;
        this.label = label;
        this.color = color;
    }

    public int getSeverity() {
        return severity;
    }

    public String getLabel() {
        return label;
    }

    public int getColor() {
        return color;
    }
}

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

for (Level level : Level.values()) {
    System.out.println("Level " + level.name()
            + "\n\tSeverity: " + level.getSeverity()
            + "\n\tLabel: " + level.getLabel()
            + "\n\tColor: " + level.getColor());
}

Что будет производить этот вывод:

Level INFO
	Severity: 1
	Label: Informative message
	Color: 43520
Level WARNING
	Severity: 2
	Label: Warning message
	Color: 16753920
Level ERROR
	Severity: 3
	Label: Error message
	Color: 10682368

Таким образом, это означает, что level.getSeverity() полностью заменяет метод getLevelValue(Level level) на switch-case, который мы видели раньше.

Разбор JSON с Enums

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

Пример данных JSON

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

[
    { 
        "level": 1,
        "timestamp": 1675867184342,
        "message": "Program started"
    },
    {
        "level": 2,
        "timestamp": 1675867185921,
        "message": "File X not found"
    },
    {
        "level": 3,
        "timestamp": 1675867186357,
        "message": "Error at line Y"
    }
]

Запись для загрузки данных JSON

Мы хотим прочитать эти сообщения в объекты Java, используя библиотеку Джексона и запись:

record LogMessage(Level level, Long timestamp, String message) {
    @JsonIgnore
    public ZonedDateTime getZonedDateTime() {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp()), ZoneId.of("UTC"));
    }
}

Дополнительный метод getZonedDateTime преобразует значение Long timestamp для использования в нашем коде, но из-за атрибута @JsonIgnore это значение не будет преобразовано обратно в JSON, как вы увидите позже.

Чтение объектов JSON в Java

Используя библиотеку Джексона, мы можем легко преобразовать эти данные JSON в массив LogMessage, если мы добавим несколько атрибутов Джексона в перечисление и с помощью ObjectMapper:

enum Level {
    ...;
    @JsonValue
    private final int severity;
    ...
    @JsonCreator
    public static Level fromValue(Integer severity) 
        throws IllegalArgumentException {
        return Arrays.stream(Level.values()).sequential()
                .filter(v -> v.getSeverity() == severity)
                .findFirst()
                .orElseThrow(() -> 
                    new IllegalArgumentException("The given severity does not exist:" + severity));
    }
}

ObjectMapper objectMapper = new ObjectMapper();
LogMessage[] logMessages = objectMapper
    .readValue(json, LogMessage[].class);

for (LogMessage logMessage : logMessages) {
    System.out.println("Log message at " 
            + logMessage.getZonedDateTime()
            + "\n\tSeverity: " 
            + logMessage.level().getLabel()
            + "\n\tMessage: " 
            + logMessage.message());
}

Это приведет к следующему выводу. Как видите, числовое значение уровня из JSON было преобразовано в значение Level-enum:

Log message at 2023-02-08T14:39:44.342Z[UTC]
        Severity: Informative message
        Message: Program started
Log message at 2023-02-08T14:39:45.921Z[UTC]
        Severity: Warning message
        Message: File X not found
Log message at 2023-02-08T14:39:46.357Z[UTC]
        Severity: Error message
        Message: Error at line Y

Преобразование объекта Java в данные JSON

Поскольку в предыдущих изменениях мы уже добавили атрибут @JsonValue к переменной severity перечисления, ObjectMapper будет использовать правильное значение для преобразования уровня обратно в JSON. Давайте используем PrettyPrinter для создания форматированного JSON из нашего массива logMessages:

System.out.println(objectMapper
    .writerWithDefaultPrettyPrinter()
    .writeValueAsString(logMessages));

Это создаст следующий вывод, который содержит то же содержимое, что и наши исходные данные JSON.

[ {
  "level" : 1,
  "timestamp" : 1675867184342,
  "message" : "Program started"
}, {
  "level" : 2,
  "timestamp" : 1675867185921,
  "message" : "File X not found"
}, {
  "level" : 3,
  "timestamp" : 1675867186357,
  "message" : "Error at line Y"
} ]

Преобразование ZonedDateTime в JSON

Как упоминалось ранее, из-за атрибута @JsonIgnore метода getZonedDateTime это значение не включается в сгенерированный JSON. И это даже нуждается в дополнительном замечании... java.time.ZonedDateTime по умолчанию не поддерживается Джексоном. Необходим дополнительный модуль com.fasterxml.jackson.datatype:jackson-datatype-jsr310.

Таким образом, необходимо внести несколько дополнительных изменений, чтобы иметь возможность экспортировать это значение, удалив @JsonIgnore, добавив @JsonFormat и зарегистрировав JavaTimeModule в objectMapper:

record LogMessage(Level level, Long timestamp, String message) {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    public ZonedDateTime getZonedDateTime() {
        return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp()), ZoneId.of("UTC"));
    }
}

objectMapper.registerModule(new JavaTimeModule());
System.out.println(objectMapper
    .writerWithDefaultPrettyPrinter()
    .writeValueAsString(logMessages));

Это генерирует JSON с ZonedDateTime в определенном формате:

[ {
  "level" : 1,
  "timestamp" : 1675867184342,
  "message" : "Program started",
  "zonedDateTime" : "08-02-2023 02:39:44"
}, {
  "level" : 2,
  "timestamp" : 1675867185921,
  "message" : "File X not found",
  "zonedDateTime" : "08-02-2023 02:39:45"
}, {
  "level" : 3,
  "timestamp" : 1675867186357,
  "message" : "Error at line Y",
  "zonedDateTime" : "08-02-2023 02:39:46"
} ]

Заключение

Перечисления Java могут содержать гораздо больше, чем просто список определений, но также данные и расширенную функциональность!

Пожалуйста, поэкспериментируйте с данным примером файла кода и узнайте, как вы можете использовать его в качестве основы для улучшения своего кода...

Первоначально опубликовано на https://webtechie.be.