在 Java 中將一個枚舉轉換為另一個枚舉
1. 概述
枚舉(或枚舉)是 Java 編程語言中一個強大且廣泛使用的功能。在某些情況下,我們可能需要將一種枚舉類型轉換為另一種枚舉類型。當我們集成不同的庫或框架、利用不同平台的微服務或使用難以更新的遺留代碼時,可能會出現這種要求。
在本文中,我們將探討在 Java 中將一個枚舉映射或轉換為另一個枚舉的不同技術。我們將研究可以提供幫助的內置機制和外部庫。
2. 定義模型
在轉換枚舉時,我們可能會遇到兩種可以使用不同實現技術的主要情況。第一種情況涉及具有不同值集的不相關枚舉。第二種情況涉及具有相同值但從 Java 角度來看代表不同類的枚舉。我們不能簡單地轉換此類類的實例,並且仍然需要執行映射。
為了說明這些技術,我們定義兩個數據模型。第一個模型代表枚舉具有相同值的場景:
public enum OrderStatus {
PENDING, APPROVED, PACKED, DELIVERED;
}
public enum CmsOrderStatus {
PENDING, APPROVED, PACKED, DELIVERED;
}
第二個模型代表枚舉具有不同值的場景:
public enum UserStatus {
PENDING, ACTIVE, BLOCKED, INACTIVATED_BY_SYSTEM, DELETED;
}
public enum ExternalUserStatus {
ACTIVE, INACTIVE
}
3.使用Java核心
大多數枚舉轉換可以使用 Java 語言的核心功能來實現,而不需要外部庫。
3.1.使用開關
最直接的選擇之一是使用switch
機制。通過為每個枚舉常量創建適當的條件,我們可以確定相應的轉換值。 switch 語句的語法隨著不同的 Java 版本而演變。根據項目的 Java 版本,可以以不同的方式實現切換。
從 Java 12 開始,引入了新的 switch 功能,包括 switch 表達式和多個 case 值。這允許我們直接返回 switch 的結果並將多個值組合成一個 case。 Java 12 有此功能的預覽版本(我們可以使用它,但需要額外的配置),而 Java 14 提供永久版本。
我們來看一個實現示例:
public ExternalUserStatus toExternalUserStatusViaSwitchStatement() {
return switch (this) {
case PENDING, BLOCKED, INACTIVATED_BY_SYSTEM, DELETED -> ExternalUserStatus.INACTIVE;
case ACTIVE -> ExternalUserStatus.ACTIVE;
};
}
然而,普通的 switch 語句仍然可以與舊版本的 Java 一起使用:
public ExternalUserStatus toExternalUserStatusViaRegularSwitch() {
switch (this) {
case PENDING:
case BLOCKED:
case INACTIVATED_BY_SYSTEM:
case DELETED:
return ExternalUserStatus.INACTIVE;
case ACTIVE:
return ExternalUserStatus.ACTIVE;
}
return null;
}
下面的測試片段演示了它是如何工作的:
@Test
void whenUsingSwitchStatement_thenEnumConverted() {
UserStatus userStatusDeleted = UserStatus.DELETED;
UserStatus userStatusPending = UserStatus.PENDING;
UserStatus userStatusActive = UserStatus.ACTIVE;
assertEquals(ExternalUserStatus.INACTIVE, userStatusDeleted.toExternalUserStatusViaSwitchStatement());
assertEquals(ExternalUserStatus.INACTIVE, userStatusPending.toExternalUserStatusViaSwitchStatement());
assertEquals(ExternalUserStatus.ACTIVE, userStatusActive.toExternalUserStatusViaSwitchStatement());
}
@Test
void whenUsingSwitch_thenEnumConverted() {
UserStatus userStatusDeleted = UserStatus.DELETED;
UserStatus userStatusPending = UserStatus.PENDING;
UserStatus userStatusActive = UserStatus.ACTIVE;
assertEquals(ExternalUserStatus.INACTIVE, userStatusDeleted.toExternalUserStatusViaRegularSwitch());
assertEquals(ExternalUserStatus.INACTIVE, userStatusPending.toExternalUserStatusViaRegularSwitch());
assertEquals(ExternalUserStatus.ACTIVE, userStatusActive.toExternalUserStatusViaRegularSwitch());
}
值得一提的是,這種轉換邏輯不一定需要駐留在枚舉類本身中,但如果可能的話最好封裝該邏輯。
3.2.使用成員變量
轉換枚舉的另一種方法是利用在枚舉內部定義字段的可能性。通過在UserStatus
中指定externalUserStatus
字段並在常量聲明中提供所需的值,我們可以為每個枚舉值定義顯式映射。在此方法中,轉換方法返回externalUserStatus
值。
UserStatus
定義如下所示:
public enum UserStatusWithFieldVariable {
PENDING(ExternalUserStatus.INACTIVE),
ACTIVE(ExternalUserStatus.ACTIVE),
BLOCKED(ExternalUserStatus.INACTIVE),
INACTIVATED_BY_SYSTEM(ExternalUserStatus.INACTIVE),
DELETED(ExternalUserStatus.INACTIVE);
private final ExternalUserStatus externalUserStatus;
UserStatusWithFieldVariable(ExternalUserStatus externalUserStatus) {
this.externalUserStatus = externalUserStatus;
}
}
轉換方法將返回the externalUserStatus
值:
public ExternalUserStatus toExternalUserStatus() {
return externalUserStatus;
}
3.3.使用EnumMap
上述方法適用於所有類型的可修改枚舉。但是,在我們無法更新源代碼或希望將轉換邏輯保留在枚舉本身之外的情況下, EnumMap
類可能會很有用。
EnumMap
將幫助我們進行獨立映射:
public class UserStatusMapper {
public static EnumMap<UserStatus, ExternalUserStatus> statusesMap;
static {
statusesMap = new EnumMap<>(UserStatus.class);
statusesMap.put(UserStatus.PENDING, ExternalUserStatus.INACTIVE);
statusesMap.put(UserStatus.BLOCKED, ExternalUserStatus.INACTIVE);
statusesMap.put(UserStatus.DELETED, ExternalUserStatus.INACTIVE);
statusesMap.put(UserStatus.INACTIVATED_BY_SYSTEM, ExternalUserStatus.INACTIVE);
statusesMap.put(UserStatus.ACTIVE, ExternalUserStatus.ACTIVE);
}
}
EnumMap 特別針對使用枚舉作為映射鍵進行了優化,最好避免使用其他類型的映射。
3.4.使用枚舉名稱
當具有相同值的枚舉之間進行轉換時,我們可以依賴Java提供的valueOf()
方法。此方法根據提供的枚舉名稱返回枚舉值。但是,請務必注意,提供的名稱必須與用於聲明枚舉常量的標識符完全匹配。如果在聲明的常量中找不到提供的名稱,則會拋出IllegalArgumentException
。
謹慎使用枚舉名稱方法非常重要,尤其是在使用來自我們無法控制的外部庫或服務的枚舉時。兩個枚舉之間的不匹配可能會導致運行時錯誤,並可能破壞服務。
為了展示解釋的方法,我們將使用OrderStatus
和CmsOrderStatus
實體:
public CmsOrderStatus toCmsOrderStatus() {
return CmsOrderStatus.valueOf(this.name());
}
3.5.使用序數法
序數方法是一種有趣但棘手的技術,它依賴於枚舉的內部實現。在內部,枚舉表示為常量數組,允許我們迭代這些值並使用索引訪問它們。
讓我們看看如何使用ordinal()
功能將OrderStatus
轉換為CmsOrderStatus
:
public CmsOrderStatus toCmsOrderStatusOrdinal() {
return CmsOrderStatus.values()[this.ordinal()];
}
並測試顯示用法:
@Test
void whenUsingOrdinalApproach_thenEnumConverted() {
OrderStatus orderStatusApproved = OrderStatus.APPROVED;
OrderStatus orderStatusDelivered = OrderStatus.DELIVERED;
OrderStatus orderStatusPending = OrderStatus.PENDING;
assertEquals(CmsOrderStatus.APPROVED, orderStatusApproved.toCmsOrderStatusOrdinal());
assertEquals(CmsOrderStatus.DELIVERED, orderStatusDelivered.toCmsOrderStatusOrdinal());
assertEquals(CmsOrderStatus.PENDING, orderStatusPending.toCmsOrderStatusOrdinal());
}
值得注意的是,序數方法有其局限性。它在編譯期間可能不會顯示任何問題,但當枚舉在大小方面變得不兼容時,它很容易出現運行時故障。即使枚舉的大小保持兼容,其值或索引的更改也可能導致錯誤和不一致的行為。
儘管序數方法適用於某些場景,但通常建議使用更穩定的方法。正如 Java 文檔中所述:
序數方法主要設計用於復雜的基於枚舉的數據結構,例如 java.util.EnumSet 和 java.util.EnumMap。
4. 使用MapStruct
MapStruct 是一個流行的庫,用於實體之間的映射,它對於枚舉轉換也很有用。
4.1. Maven依賴
讓我們將 MapStruct 依賴項添加到pom.xml
中。我們需要MapStruct library
作為依賴項,並需要MapStruct Processor
作為註釋處理器:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
我們可以在 Maven 中央存儲庫中找到最新版本的MapStruct及其處理器.
4.2.用法
MapStruct 根據接口中定義的註釋生成實現。我們可以在枚舉值完全不同的情況以及值相同的情況下使用此庫。如果未指定特定映射,MapStruct 會自動嘗試根據常量名稱之間的匹配進行映射。同時,我們可以配置枚舉中值之間的顯式映射。使用較新的 MapStruct 版本,我們應該對枚舉使用@ValueMapping
而不是@Mapping
。
考慮到我們的模型,可以定義OrderStatus
和CmsOrderStatus
的映射,而無需任何手動映射。 UserStatus
和ExternalUserStatus
之間的映射需要額外的配置,其中枚舉名稱不匹配:
@Mapper
public interface EnumMapper {
CmsOrderStatus map(OrderStatus orderStatus);
@ValueMapping(source = "PENDING", target = "INACTIVE")
@ValueMapping(source = "BLOCKED", target = "INACTIVE")
@ValueMapping(source = "INACTIVATED_BY_SYSTEM", target = "INACTIVE")
@ValueMapping(source = "DELETED", target = "INACTIVE")
ExternalUserStatus map(UserStatus userStatus);
}
同時,將自動生成UserStatus.ACTIVE
和ExternalUserStatus.ACTIVE
的映射。
除此之外,我們可以利用MappingConstants.ANY_REMAINING
功能,為映射末尾尚未映射的枚舉常量設置默認值(在我們的例子中為ExternalUserStatus
. INACTIVE
):
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "INACTIVE")
ExternalUserStatus mapDefault(UserStatus userStatus);
5. 最佳實踐和用例
選擇合適的轉換策略取決於項目的具體要求。應考慮諸如枚舉常量之間的相似性、對源代碼的訪問以及枚舉更改的頻率等因素。
當我們可以訪問枚舉的源代碼時,建議使用開關和成員變量方法。這些方法使我們能夠將轉換邏輯封裝在單個位置,從而提供更好的代碼組織。此外,可以擴展開關方法以使用一些外部屬性並執行更複雜的映射邏輯。但是,需要注意的是,在條件繁重和枚舉量巨大的情況下,使用 switch 方法可能會導致代碼冗長且難以管理。
當我們無法訪問源代碼或想要清楚地可視化映射而不需要冗長的 switch 語句時, EnumMap
方法是一個不錯的選擇。 EnumMap
允許我們定義枚舉之間的顯式映射,而不需要大量的 switch 語句。當轉換邏輯簡單時它特別有用。
僅當源枚舉名稱和目標枚舉名稱相同時才應使用枚舉名稱方法。在可能存在名稱不匹配的情況下,最好通過合併附加邏輯和定義默認行為來擴展映射方法。這確保了轉換可以妥善處理不匹配和意外情況。
序數方法雖然在映射邏輯基於索引的場景中很有用,但由於其固有的風險,通常應避免。枚舉名稱和序數方法都需要彈性實現來應對枚舉大小或值的變化。
或者,為了避免維護複雜的 switch 語句或映射,可以使用 MapStruct 來自動化該過程。它可以處理具有相同名稱和完全不同名稱的枚舉,允許我們定義額外的映射邏輯和默認行為。
無論選擇哪種方法,一旦枚舉發生更改就更新映射至關重要。如果更新映射不可行,請確保代碼設計為適當處理枚舉更改,方法是回退到默認映射值或在出現新枚舉值時優雅地處理它們。這種方法確保了枚舉轉換的壽命和穩定性。
六,結論
在本文中,我們探討了在 Java 中映射枚舉的各種技術。我們討論了內置機制和 MapStruct 的使用。根據特定的用例和要求,不同的技術可能比其他技術更合適。建議在選擇轉換策略時考慮多個因素,例如枚舉常量之間的相似性、對源代碼的訪問以及枚舉更改的可能性。
完整的示例可以在 GitHub 上找到。