Erweitern von Enums in Java

1. Übersicht

Der in Java 5 eingeführte Aufzählungstyp ist ein spezieller Datentyp, der eine Gruppe von Konstanten darstellt.

Mithilfe von Aufzählungen können wir unsere Konstanten für die Typensicherheit definieren und verwenden. Die Konstanten werden zur Kompilierungszeit überprüft.

Außerdem können wir die Konstanten in der switch-case- Anweisung verwenden.

In diesem Tutorial werden wir die Erweiterung von Enums in Java diskutieren, indem wir beispielsweise neue konstante Werte und neue Funktionen hinzufügen.

2. Aufzählungen und Vererbung

Wenn wir eine Java-Klasse erweitern möchten, erstellen wir normalerweise eine Unterklasse. In Java sind Aufzählungen ebenfalls Klassen.

In diesem Abschnitt wollen wir sehen, ob wir eine Aufzählung wie bei regulären Java-Klassen erben können.

2.1. Erweitern eines Aufzählungstyps

Schauen wir uns zunächst ein Beispiel an, damit wir das Problem schnell verstehen können:

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 }

Wie der obige Code zeigt, haben wir eine Aufzählung BasicStringOperation , die drei grundlegende Zeichenfolgenoperationen enthält.

Angenommen , wir möchten der Aufzählung eine Erweiterung hinzufügen, z. B. MD5_ENCODE und BASE64_ENCODE . Wir können diese einfache Lösung finden:

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 }

Wenn wir jedoch versuchen, die Klasse zu kompilieren, wird der Compilerfehler angezeigt:

Cannot inherit from enum BasicStringOperation

2.2. Vererbung ist für Aufzählungen nicht zulässig

Lassen Sie uns nun herausfinden, warum wir unseren Compilerfehler erhalten haben.

Wenn wir eine Aufzählung kompilieren, macht der Java-Compiler etwas Magisches daran:

  • Es verwandelt die Aufzählung in eine Unterklasse der abstrakten Klasse java.lang.Enum
  • Es kompiliert die Enum als letzte Klasse

Wenn wir beispielsweise unsere kompilierte BasicStringOperation-Enumeration mit javap zerlegen , wird sie als Unterklasse von java.lang.Enum dargestellt :

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

Wie wir wissen, können wir in Java keine letzte Klasse erben . Selbst wenn wir die ExtendedStringOperation- Enumeration erstellen könnten, um BasicStringOperation zu erben , würde unsere ExtendedStringOperation- Enumeration zwei Klassen erweitern: BasicStringOperation und java.lang.Enum. Das heißt, es würde eine Mehrfachvererbungssituation werden, die in Java nicht unterstützt wird.

3. Emulieren Sie erweiterbare Aufzählungen mit Schnittstellen

Wir haben gelernt, dass wir keine Unterklasse einer vorhandenen Aufzählung erstellen können. Eine Schnittstelle ist jedoch erweiterbar. Daher können wir erweiterbare Aufzählungen emulieren, indem wir eine Schnittstelle implementieren .

3.1. Emulieren Erweitern der Konstanten

Um diese Technik schnell zu verstehen, schauen wir uns an, wie Sie die Erweiterung unserer BasicStringOperation-Enumeration auf MD5_ENCODE- und BASE64_ENCODE- Operationen emulieren können .

Zuerst erstellen wir eine Schnittstelle StringOperation :

public interface StringOperation { String getDescription(); } 

Als nächstes lassen wir beide Aufzählungen die obige Schnittstelle implementieren:

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 } 

Schauen wir uns zum Schluss an, wie eine erweiterbare BasicStringOperation-Enumeration emuliert wird .

Angenommen , wir haben eine Methode in unserer Anwendung, um die Beschreibung der BasicStringOperation-Enumeration abzurufen :

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

Jetzt können wir den Parametertyp BasicStringOperation in den Schnittstellentyp StringOperation ändern , damit die Methode Instanzen aus beiden Aufzählungen akzeptiert:

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

3.2. Funktionen erweitern

Wir haben gesehen, wie man erweiterte Konstanten von Aufzählungen mit Schnittstellen emuliert.

Außerdem können wir der Schnittstelle Methoden hinzufügen, um die Funktionen der Aufzählungen zu erweitern.

Zum Beispiel möchten wir unsere StringOperation- Enums erweitern, damit jede Konstante die Operation tatsächlich auf einen bestimmten String anwenden kann:

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

Um dies zu erreichen, fügen wir zunächst die Methode apply () zur Schnittstelle hinzu:

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

Als Nächstes lassen wir jede StringOperation-Enumeration diese Methode implementieren:

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())); } }; //... } 

Eine Testmethode beweist, dass dieser Ansatz wie erwartet funktioniert:

@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. Erweitern einer Aufzählung, ohne den Code zu ändern

Wir haben gelernt, wie man eine Enumeration durch die Implementierung von Schnittstellen erweitert.

Manchmal möchten wir jedoch die Funktionen einer Aufzählung erweitern, ohne sie zu ändern. Zum Beispiel möchten wir eine Aufzählung aus einer Bibliothek eines Drittanbieters erweitern.

4.1. Verknüpfen von Enum-Konstanten und Schnittstellenimplementierungen

Schauen wir uns zunächst ein Beispiel für eine Aufzählung an:

public enum ImmutableOperation { REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE } 

Angenommen, die Aufzählung stammt aus einer externen Bibliothek, daher können wir den Code nicht ändern.

Jetzt möchten wir in unserer Application- Klasse eine Methode haben, um die angegebene Operation auf die Eingabezeichenfolge anzuwenden:

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.

Wie immer ist der vollständige Quellcode des Artikels auf GitHub verfügbar.