«1. Обзор

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

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

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

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

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

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

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

2.1. Расширение типа Enum

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

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
}

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

Теперь предположим, что мы хотим добавить какое-то расширение к перечислению, например, 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 делает с ним какое-то волшебство:

    Он превращает перечисление в подкласс абстрактного класса java.lang.Enum. Он компилирует перечисление как окончательный класс

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

$ javap BasicStringOperation  
public final class com.baeldung.enums.extendenum.BasicStringOperation 
    extends java.lang.Enum<com.baeldung.enums.extendenum.BasicStringOperation> {
  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;
 ...
}

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

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

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

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

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

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

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
}

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

Допустим, в нашем приложении есть метод для получения описания перечисления BasicStringOperation:

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

Теперь мы можем изменить тип параметра BasicStringOperation на тип интерфейса StringOperation, чтобы метод принимал экземпляры из обоих перечислений: ~ ~~

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

3.2. Расширение функциональности

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

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

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

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

Для этого сначала добавим в интерфейс метод apply():

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

Затем мы позволяем каждому перечислению StringOperation реализовать этот метод:

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);
}

Затем мы создадим сопоставление между константами перечисления и реализациями оператора, используя EnumMap\u003cImmutableOperation, Operator\u003e:

public class Application {
    private static final Map<ImmutableOperation, Operator> 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);
    }

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

@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. В этой статье мы обсудили, почему enum не поддерживает наследование. После этого мы рассмотрели, как эмулировать расширяемые перечисления с интерфейсами.

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

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