用Java擴展枚舉

1.概述

Java 5中引入的enum類型是一種特殊的數據類型,它代表一組常量。

使用枚舉,我們可以以類型安全的方式定義和使用常量。它為常量帶來了編譯時檢查。

此外,它允許我們在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_ENCODEBASE64_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
  • 它將枚舉編譯為final

例如,如果我們使用javap BasicStringOperation枚舉,則會看到它表示為java.lang.Enum<BasicStringOperation>的子類:

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

 ...

 }

眾所周知,我們無法繼承Java中final而且,即使我們可以創建ExtendedStringOperation枚舉以繼承BasicStringOperation ,我們的ExtendedStringOperation枚舉也會擴展兩個類: BasicStringOperationjava.lang.Enum.也就是說,它將成為多重繼承的情況,Java不支持這種情況。

3.使用接口模擬可擴展枚舉

我們了解到我們無法創建現有枚舉的子類。但是,接口是可擴展的。因此,我們可以通過實現interface來模擬可擴展的枚舉

3.1。模擬擴展常數

為了快速理解該技術,讓我們看一下如何模擬將BasicStringOperation枚舉擴展為具有MD5_ENCODEBASE64_ENCODE操作。

首先,讓我們創建一個interface 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<ImmutableOperation, Operator> 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由於我們的驗證是在static塊中進行的,因此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中的一種特殊數據類型。在本文中,我們討論了為什麼枚舉不支持繼承。之後,我們介紹瞭如何使用接口模擬可擴展枚舉。

另外,我們還學習瞭如何在不更改枚舉的情況下擴展其功能。