Разширяване на Enums в Java

1. Общ преглед

Типът enum, въведен в Java 5, е специален тип данни, който представлява група константи.

Използвайки изброявания, можем да дефинираме и използваме нашите константи по пътя на безопасността на типа. Той носи проверка по време на компилация на константите.

Освен това, това ни позволява да използваме константите в оператора за превключване .

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

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

Когато искаме да разширим клас Java, обикновено създаваме подклас. В Java преброяванията също са класове.

В този раздел нека видим дали можем да наследяваме enum, както правим с обикновените 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 }

Както показва горният код, имаме enum 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. Наследяването не е разрешено за Enums

Сега нека да разберем защо имаме грешка в компилатора.

Когато компилираме enum, Java компилаторът му прави някаква магия:

  • Превръща enum в подклас на абстрактния клас 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 enum, за да има операции 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 enum:

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

Сега можем да променим типа на параметъра BasicStringOperation в интерфейсния тип StringOperation, за да накараме метода да приема екземпляри от двата изброявания:

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

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

Видяхме как да подражаваме на разширяващи се константи на enum с интерфейси.

Освен това можем да добавим методи към интерфейса за разширяване на функционалностите на изброяванията.

Например, ние искаме да разширим нашите 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. Разширяване на Enum без промяна на кода

Научихме как да разширяваме enum чрез прилагане на интерфейси.

Понякога обаче искаме да разширим функционалностите на едно преброяване, без да го променяме. Например бихме искали да разширим enum от библиотека на трети страни.

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.