Розширення Enum в Java

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 :

$ 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; ... } 

Як ми знаємо, ми не можемо успадкувати остаточний клас на 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) {...}

Since we can't change the enum code, we can use EnumMap to associate the enum constants and required implementations.

First, let's create an interface:

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

Next, we'll create the mapping between enum constants and the Operator implementations using an EnumMap:

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

In this way, our applyImmutableOperation() method can apply the corresponding operation to the given input string:

@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. Validating the EnumMap Object

Now, if the enum is from an external library, we don't know if it has been changed or not, such as by adding new constants to the enum. In this case, if we don't change our initialization of the EnumMap to contain the new enum value, our EnumMap approach may run into a problem if the newly added enum constant is passed to our application.

To avoid that, we can validate the EnumMap after its initialization to check if it contains all enum constants:

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

As the code above shows, if any constant from ImmutableOperation is not mapped, an IllegalStateException will be thrown. Since our validation is in a static block, IllegalStateException will be the cause of ExceptionInInitializerError:

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

Thus, once the application fails to start with the mentioned error and cause, we should double-check the ImmutableOperation to make sure all constants are mapped.

5. Conclusion

The enum is a special data type in Java. In this article, we've discussed why enum doesn't support inheritance. After that, we addressed how to emulate extensible enums with interfaces.

Also, we've learned how to extend the functionalities of an enum without changing it.

Як завжди, повний вихідний код статті доступний на GitHub.