1. 概述
枚举(enum)是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 14+**(正式版)或 **Java 12+**(预览版)支持switch表达式和多值case:
public ExternalUserStatus toExternalUserStatusViaSwitchStatement() { return switch (this) { case PENDING, BLOCKED, INACTIVATED_BY_SYSTEM, DELETED -> ExternalUserStatus.INACTIVE; case ACTIVE -> ExternalUserStatus.ACTIVE; }; }
❌ 传统Java版本需用普通switch:
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
字段,在常量声明中指定映射值:
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;
}
}
转换方法:
public ExternalUserStatus toExternalUserStatus() {
return externalUserStatus;
}
3.3 使用EnumMap
当无法修改源码或需将转换逻辑外置时,EnumMap
是理想选择。专为枚举键优化,避免使用其他Map类型:
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);
}
}
3.4 使用枚举名称
当枚举值完全相同时,可利用Java的valueOf()
方法。注意名称必须严格匹配,否则抛出IllegalArgumentException
:
public CmsOrderStatus toCmsOrderStatus() {
return CmsOrderStatus.valueOf(this.name());
}
⚠️ 谨慎使用此方案,尤其处理外部枚举时,名称不匹配会导致运行时错误。
3.5 使用序数方法
序数方法利用枚举内部数组实现索引访问,但存在风险:
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文档明确指出:
序数方法主要用于
EnumSet
和EnumMap
等复杂数据结构。
4. 使用MapStruct
MapStruct是流行的实体映射库,也适用于枚举转换。
4.1 Maven依赖
添加MapStruct依赖和注解处理器:
<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>
4.2 使用方式
MapStruct基于注解生成实现。自动处理同名枚举,需显式配置不同名映射:
@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
会自动映射。
使用ANY_REMAINING
设置默认值:
@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "INACTIVE")
ExternalUserStatus mapDefault(UserStatus userStatus);
5. 最佳实践和使用场景
选择转换策略需考虑项目需求:
- 枚举常量相似度
- 源码访问权限
- 枚举变更频率
推荐方案对比
场景 | 推荐方案 | 优点 | 缺点 |
---|---|---|---|
✅ 可修改源码 | Switch/成员变量 | 逻辑封装,代码组织好 | 复杂条件时代码冗长 |
✅ 不可修改源码 | EnumMap | 映射清晰,无需switch | 需维护映射关系 |
✅ 枚举名称完全相同 | 枚举名称方法 | 简单直接 | 名称不匹配时崩溃 |
❌ 避免使用 | 序数方法 | 特定场景高效 | 枚举变更时风险高 |
✅ 自动化需求 | MapStruct | 支持同名/异名枚举,可配置默认值 | 引入额外依赖 |
关键原则
- 枚举变更时立即更新映射
- 无法更新时:
- 设置默认映射值
- 优雅处理新枚举值
- 优先选择稳定方案:避免序数方法,谨慎使用名称匹配
6. 结论
本文探讨了Java中枚举映射的多种技术,包括内置机制和MapStruct库。没有万能方案,需根据具体场景选择:
- 同名枚举 → 枚举名称方法
- 可修改源码 → Switch/成员变量
- 不可修改源码 → EnumMap
- 复杂映射需求 → MapStruct
完整示例代码见GitHub仓库。