Рубрики
Без рубрики

Расширение перечислений в Java

Узнайте, как расширить перечисления в Java.

Автор оригинала: Kai Yuan.

1. Обзор

Тип перечисления, введенный в Java 5, представляет собой специальный тип данных, представляющий группу констант.

Используя перечисления, мы можем определять и использовать наши константы в целях безопасности типов. Это приводит к проверке констант во время компиляции.

Кроме того, это позволяет нам использовать константы в операторе switch-case .

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

2. Перечисления и наследование

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

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

2.1. Расширение типа перечисления

Прежде всего, давайте рассмотрим пример, чтобы мы могли быстро понять проблему:

public enum BasicStringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;

    // constructor and getter
}

Как показано в приведенном выше коде, у нас есть перечисление Базовая строковая операция , которая содержит три основные строковые операции.

Теперь предположим, что мы хотим добавить некоторое расширение в перечисление, например MD5_ENCODE и BASE64_ENCODE . Мы можем придумать это простое решение:

public enum ExtendedStringOperation extends BasicStringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter
}

Однако, когда мы попытаемся скомпилировать класс, мы увидим ошибку компилятора:

Cannot inherit from enum BasicStringOperation

2.2. Наследование Не допускается для Перечислений

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

Когда мы компилируем перечисление, компилятор Java делает с ним какое-то волшебство:

  • Это превращает перечисление в подкласс абстрактного класса Это превращает перечисление в подкласс абстрактного класса
  • Он компилирует перечисление как окончательный класс

Например, если мы разберем нашу скомпилированную Базовую строковую операцию enum с помощью javap , мы увидим, что она представлена как подкласс java.lang.Enum :

$ javap BasicStringOperation  
public final class com.baeldung.enums.extendenum.BasicStringOperation 
    extends java.lang.Enum {
  public static final com.baeldung.enums.extendenum.BasicStringOperation TRIM;
  public static final com.baeldung.enums.extendenum.BasicStringOperation TO_UPPER;
  public static final com.baeldung.enums.extendenum.BasicStringOperation REVERSE;
 ...
}

Как мы знаем, мы не можем наследовать класс final в Java. Более того, даже если бы мы могли создать Расширенную строковую операцию перечисление для наследования Базовой строковой операции , наш ExtendedStringOperation перечисление расширило бы два класса: BasicStringOperation и java.lang.Enum. То есть это станет ситуацией множественного наследования, которая не поддерживается в Java.

3. Эмуляция Расширяемых Перечислений С Помощью Интерфейсов

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

3.1. Эмуляция расширения констант

Чтобы быстро понять этот метод, давайте посмотрим, как эмулировать расширение нашей Базовой строковой операции перечисления, чтобы иметь MD5_ENCODE и BASE64_ENCODE операции.

Во-первых, давайте создадим интерфейс Строковую операцию :

public interface StringOperation {
    String getDescription();
}

Затем мы сделаем так, чтобы оба перечисления реализовали интерфейс выше:

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces."),
    TO_UPPER("Changing all characters into upper case."),
    REVERSE("Reversing the given string.");

    private String description;
    // constructor and getter override
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");

    private String description;

    // constructor and getter override
}

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

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

public class Application {
    public String getOperationDescription(BasicStringOperation stringOperation) {
        return stringOperation.getDescription();
    }
}

Теперь мы можем изменить тип параметра Базовая строковая операция в тип интерфейса Строковая операция , чтобы метод принимал экземпляры из обоих перечислений:

public String getOperationDescription(StringOperation stringOperation) {
    return stringOperation.getDescription();
}

3.2. Расширение функциональных возможностей

Мы видели, как эмулировать расширяющиеся константы перечислений с помощью интерфейсов.

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

Например, мы хотим расширить нашу операцию String перечисления, чтобы каждая константа могла фактически применить операцию к данной строке:

public class Application {
    public String applyOperation(StringOperation operation, String input) {
        return operation.apply(input);
    }
    //...
}

Для этого, во-первых, давайте добавим метод apply() в интерфейс:

public interface StringOperation {
    String getDescription();
    String apply(String input);
}

Затем мы позволим каждой операции String enum реализовать этот метод:

public enum BasicStringOperation implements StringOperation {
    TRIM("Removing leading and trailing spaces.") {
        @Override
        public String apply(String input) { 
            return input.trim(); 
        }
    },
    TO_UPPER("Changing all characters into upper case.") {
        @Override
        public String apply(String input) {
            return input.toUpperCase();
        }
    },
    REVERSE("Reversing the given string.") {
        @Override
        public String apply(String input) {
            return new StringBuilder(input).reverse().toString();
        }
    };

    //...
}

public enum ExtendedStringOperation implements StringOperation {
    MD5_ENCODE("Encoding the given string using the MD5 algorithm.") {
        @Override
        public String apply(String input) {
            return DigestUtils.md5Hex(input);
        }
    },
    BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.") {
        @Override
        public String apply(String input) {
            return new String(new Base64().encode(input.getBytes()));
        }
    };

    //...
}

Тестовый метод доказывает, что этот подход работает так, как мы ожидали:

@Test
public void givenAStringAndOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " hello";
    String expectedToUpper = " HELLO";
    String expectedReverse = "olleh ";
    String expectedTrim = "hello";
    String expectedBase64 = "IGhlbGxv";
    String expectedMd5 = "292a5af68d31c10e31ad449bd8f51263";
    assertEquals(expectedTrim, app.applyOperation(BasicStringOperation.TRIM, input));
    assertEquals(expectedToUpper, app.applyOperation(BasicStringOperation.TO_UPPER, input));
    assertEquals(expectedReverse, app.applyOperation(BasicStringOperation.REVERSE, input));
    assertEquals(expectedBase64, app.applyOperation(ExtendedStringOperation.BASE64_ENCODE, input));
    assertEquals(expectedMd5, app.applyOperation(ExtendedStringOperation.MD5_ENCODE, input));
}

4. Расширение перечисления Без изменения кода

Мы научились расширять перечисление, реализуя интерфейсы.

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

4.1. Связывание констант перечисления и реализаций интерфейса

Во – первых, давайте рассмотрим пример перечисления:

public enum ImmutableOperation {
    REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}

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

Теперь в нашем классе Application мы хотим иметь метод для применения данной операции к входной строке:

public String applyImmutableOperation(ImmutableOperation operation, String input) {...}

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

Во-первых, давайте создадим интерфейс:

public interface Operator {
    String apply(String input);
}

Затем мы создадим сопоставление между константами перечисления и реализациями Operator с помощью EnumMap Operator> : Operator>

public class Application {
    private static final Map OPERATION_MAP;

    static {
        OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
        OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
        OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
        OPERATION_MAP.put(ImmutableOperation.REMOVE_WHITESPACES, input -> input.replaceAll("\\s", ""));
    }

    public String applyImmutableOperation(ImmutableOperation operation, String input) {
        return operationMap.get(operation).apply(input);
    }

Таким образом, наш метод apply Immutable Operation() может применить соответствующую операцию к данной входной строке:

@Test
public void givenAStringAndImmutableOperation_whenApplyOperation_thenGetExpectedResult() {
    String input = " He ll O ";
    String expectedToLower = " he ll o ";
    String expectedRmWhitespace = "HellO";
    String expectedInvertCase = " hE LL o ";
    assertEquals(expectedToLower, app.applyImmutableOperation(ImmutableOperation.TO_LOWER, input));
    assertEquals(expectedRmWhitespace, app.applyImmutableOperation(ImmutableOperation.REMOVE_WHITESPACES, input));
    assertEquals(expectedInvertCase, app.applyImmutableOperation(ImmutableOperation.INVERT_CASE, input));
}

4.2. Проверка объекта EnumMap

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

Чтобы избежать этого, мы можем проверить EnumMap после его инициализации, чтобы проверить, содержит ли он все константы перечисления:

static {
    OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
    OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
    OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
    // ImmutableOperation.REMOVE_WHITESPACES is not mapped

    if (Arrays.stream(ImmutableOperation.values()).anyMatch(it -> !OPERATION_MAP.containsKey(it))) {
        throw new IllegalStateException("Unmapped enum constant found!");
    }
}

Как показано в приведенном выше коде, если какая-либо константа из ImmutableOperation не сопоставлена, будет выдано исключение IllegalStateException . Поскольку наша проверка находится в статическом блоке, IllegalStateException будет причиной ExceptionInInitializerError :

@Test
public void givenUnmappedImmutableOperationValue_whenAppStarts_thenGetException() {
    Throwable throwable = assertThrows(ExceptionInInitializerError.class, () -> {
        ApplicationWithEx appEx = new ApplicationWithEx();
    });
    assertTrue(throwable.getCause() instanceof IllegalStateException);
}

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

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

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

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

Как всегда, полный исходный код статьи доступен на GitHub .