Перейти к основному содержимому

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

· 7 мин. чтения

Задача: Наибольшая подстрока палиндром

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

ANDROMEDA 42

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
}

Как видно из приведенного выше кода, у нас есть перечисление 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<BasicStringOperation> :

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

Как мы знаем, в Java нельзя наследовать конечный класс. Более того, даже если бы мы могли создать перечисление 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. Связывание констант Enum и реализаций интерфейса

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

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<ImmutableOperation, Operator> :

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 .