Java中enum做双向映射结构与优化
Java 中的
enum适合做双向映射结构,尤其是当需要在枚举常量与对应的值(如数字、字符串)之间相互转换时,通过在枚举中定义字段和转换方法,能高效实现双向映射。举例:用 enum 实现 “订单状态码” 与 “状态名称” 的双向映射
public enum OrderStatus {
// 枚举常量:状态码 + 状态名称
PENDING(1, "待支付"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消");
// 定义字段:存储映射的值
private final int code;
private final String name;
// 构造方法:初始化字段
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
// 正向映射:通过枚举常量获取code/name
public int getCode() {
return code;
}
public String getName() {
return name;
}
// 反向映射1:通过code获取枚举常量
public static OrderStatus getByCode(int code) {
for (OrderStatus status : values()) {
if (status.code == code) {
return status;
}
}
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
// 反向映射2:通过name获取枚举常量
public static OrderStatus getByName(String name) {
for (OrderStatus status : values()) {
if (status.name.equals(name)) {
return status;
}
}
throw new IllegalArgumentException("无效的订单状态名称:" + name);
}
// 测试双向映射
public static void main(String[] args) {
// 正向映射:从枚举到code/name
OrderStatus status = OrderStatus.PAID;
System.out.println("状态码:" + status.getCode()); // 输出:2
System.out.println("状态名称:" + status.getName()); // 输出:已支付
// 反向映射:从code到枚举
OrderStatus statusByCode = OrderStatus.getByCode(3);
System.out.println("对应枚举:" + statusByCode); // 输出:SHIPPED
// 反向映射:从name到枚举
OrderStatus statusByName = OrderStatus.getByName("已取消");
System.out.println("对应枚举:" + statusByName); // 输出:CANCELLED
}
}
为什么适合?
- 简洁性:枚举常量本身是固定的,字段和转换方法可直接定义在枚举内部,无需额外的 Map 等结构存储映射关系。
- 安全性:枚举常量是单例的,避免了重复实例化问题,且反向映射时通过遍历
values()(或预存 Map 优化性能),可确保映射唯一。 - 可读性:相比单独的常量类 + Map 映射,枚举的双向映射逻辑更集中,代码更易维护。
优化建议
如果枚举常量较多(如超过 10 个),遍历
values()进行反向映射可能影响性能,可在枚举中预存一个Map缓存映射关系,例如:private static final Map<Integer, OrderStatus> CODE_MAP = new HashMap<>();
static {
for (OrderStatus status : values()) {
CODE_MAP.put(status.code, status);
}
}
public static OrderStatus getByCode(int code) {
OrderStatus status = CODE_MAP.get(code);
if (status == null) {
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
return status;
}
通过静态代码块初始化 Map,可将反向映射的时间复杂度从 O (n) 优化为 O (1)。
当枚举常量较多时,每次调用
getByCode()或getByName()都遍历values()确实会影响性能,尤其是在高频调用场景下。
在枚举内部创建 Map 缓存是非常好的优化方案,这样可以将时间复杂度从 O (n) 降低到 O (1)。以下是优化后的实现:
public enum OrderStatus {
// 枚举常量:状态码 + 状态名称
PENDING(1, "待支付"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消"),
REFUNDING(6, "退款中"),
REFUNDED(7, "已退款"),
EXCHANGING(8, "换货中"),
EXCHANGED(9, "已换货"),
CONFIRMED(10, "已确认"),
APPRAISED(11, "已评价");
// 定义字段:存储映射的值
private final int code;
private final String name;
// 缓存:code到枚举的映射
private static final Map<Integer, OrderStatus> CODE_MAP = new HashMap<>();
// 缓存:name到枚举的映射
private static final Map<String, OrderStatus> NAME_MAP = new HashMap<>();
// 静态初始化块:初始化缓存
static {
for (OrderStatus status : values()) {
CODE_MAP.put(status.code, status);
NAME_MAP.put(status.name, status);
}
}
// 构造方法:初始化字段
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
// 正向映射:通过枚举常量获取code/name
public int getCode() {
return code;
}
public String getName() {
return name;
}
// 反向映射1:通过code获取枚举常量(O(1)复杂度)
public static OrderStatus getByCode(int code) {
OrderStatus status = CODE_MAP.get(code);
if (status == null) {
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
return status;
}
// 反向映射2:通过name获取枚举常量(O(1)复杂度)
public static OrderStatus getByName(String name) {
OrderStatus status = NAME_MAP.get(name);
if (status == null) {
throw new IllegalArgumentException("无效的订单状态名称:" + name);
}
return status;
}
// 测试双向映射
public static void main(String[] args) {
// 正向映射:从枚举到code/name
OrderStatus status = OrderStatus.PAID;
System.out.println("状态码:" + status.getCode()); // 输出:2
System.out.println("状态名称:" + status.getName()); // 输出:已支付
// 反向映射:从code到枚举
OrderStatus statusByCode = OrderStatus.getByCode(3);
System.out.println("对应枚举:" + statusByCode); // 输出:SHIPPED
// 反向映射:从name到枚举
OrderStatus statusByName = OrderStatus.getByName("已取消");
System.out.println("对应枚举:" + statusByName); // 输出:CANCELLED
}
}
优化说明:
-
增加了两个静态 Map 作为缓存:
CODE_MAP:存储状态码到枚举实例的映射NAME_MAP:存储状态名称到枚举实例的映射
-
使用静态初始化块初始化缓存:
- 枚举加载时执行一次,遍历所有枚举值并放入 Map
- 利用了枚举的特性:枚举常量在类加载时就已初始化完成
-
改进了反向映射方法:
- 从 Map 中直接获取,时间复杂度变为 O (1)
- 避免了每次调用都遍历所有枚举值的性能开销
这种实现方式的优点:
- 性能更好:尤其适合枚举常量多、调用频繁的场景
- 代码更简洁:查找逻辑更清晰
- 线程安全:静态初始化块在类加载时执行,天然线程安全
- 易于维护:新增枚举常量时无需修改映射逻辑
对于包含 10 个以上常量的枚举,这种优化能显著提升反向映射的性能。
除了使用 Map 缓存,还有其他几种优化枚举双向映射性能的方法,这些方法各有适用场景,可根据实际需求选择:
1. 利用数组下标映射(适用于连续整数编码)
如果状态码是连续的整数(如 1,2,3...N),可以用数组下标直接映射,访问效率比 Map 更高(数组随机访问时间复杂度 O (1)):
public enum OrderStatus {
// 注意:状态码从1开始,数组需要多留一个位置
PENDING(1, "待支付"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消");
private final int code;
private final String name;
// 用数组缓存(状态码作为下标)
private static final OrderStatus[] CODE_CACHE;
static {
// 找到最大的状态码,确定数组长度
int maxCode = 0;
for (OrderStatus status : values()) {
if (status.code > maxCode) {
maxCode = status.code;
}
}
CODE_CACHE = new OrderStatus[maxCode + 1]; // 下标从0开始
// 填充数组
for (OrderStatus status : values()) {
CODE_CACHE[status.code] = status;
}
}
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
// 通过code获取枚举(数组直接访问)
public static OrderStatus getByCode(int code) {
if (code < 0 || code >= CODE_CACHE.length || CODE_CACHE[code] == null) {
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
return CODE_CACHE[code];
}
// 其他方法...
public int getCode() { return code; }
public String getName() { return name; }
}
优点:数组访问比 Map 更快,内存占用更小
缺点:仅适用于连续的整数编码,且编码不能过大(否则数组会浪费空间)
缺点:仅适用于连续的整数编码,且编码不能过大(否则数组会浪费空间)
2. 使用枚举自带的valueOf()方法(适用于名称映射)
如果 "状态名称" 与枚举常量名一致,可以直接使用枚举自带的
valueOf(String name)方法,无需自定义映射:public enum OrderStatus {
// 枚举名直接使用状态名称(英文)
待支付(1),
已支付(2),
已发货(3),
已完成(4),
已取消(5);
private final int code;
private static final Map<Integer, OrderStatus> CODE_MAP = new HashMap<>();
static {
for (OrderStatus status : values()) {
CODE_MAP.put(status.code, status);
}
}
OrderStatus(int code) {
this.code = code;
}
public int getCode() { return code; }
// 通过code映射(仍用Map)
public static OrderStatus getByCode(int code) {
OrderStatus status = CODE_MAP.get(code);
if (status == null) {
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
return status;
}
}
// 使用时直接通过枚举名获取(无需自定义getByName)
OrderStatus status = OrderStatus.valueOf("已支付");
优点:利用枚举原生方法,无需额外代码
缺点:仅适用于 "状态名称" 与枚举常量名一致的场景,灵活性低
缺点:仅适用于 "状态名称" 与枚举常量名一致的场景,灵活性低
3. 提前初始化缓存(枚举加载时完成)
本质上与 Map 缓存思路一致,但可以通过静态内部类实现 "懒加载"(枚举本身是饿汉式加载,这里主要是优化缓存初始化时机):
public enum OrderStatus {
PENDING(1, "待支付"),
PAID(2, "已支付"),
// ...其他常量
private final int code;
private final String name;
// 静态内部类保存缓存(枚举加载时初始化)
private static class Cache {
static final Map<Integer, OrderStatus> CODE_MAP = new HashMap<>();
static final Map<String, OrderStatus> NAME_MAP = new HashMap<>();
static {
for (OrderStatus status : OrderStatus.values()) {
CODE_MAP.put(status.code, status);
NAME_MAP.put(status.name, status);
}
}
}
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
public static OrderStatus getByCode(int code) {
OrderStatus status = Cache.CODE_MAP.get(code);
if (status == null) {
throw new IllegalArgumentException("无效的订单状态码:" + code);
}
return status;
}
// 其他方法...
}
优点:通过内部类隔离缓存逻辑,代码结构更清晰
缺点:本质仍是 Map 缓存,性能与直接定义 Map 相当
缺点:本质仍是 Map 缓存,性能与直接定义 Map 相当
4. 使用第三方库(如 Apache Commons Lang)
如果项目中已引入工具库,可以利用其提供的枚举工具类简化映射逻辑,避免重复编码:
import org.apache.commons.lang3.EnumUtils;
public enum OrderStatus {
PENDING(1, "待支付"),
PAID(2, "已支付"),
// ...其他常量
private final int code;
private final String name;
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
// 通过code获取枚举(利用工具类)
public static OrderStatus getByCode(int code) {
return EnumUtils.getEnum(OrderStatus.class, s -> s.code == code);
}
// 其他方法...
}
优点:简化代码,无需手动维护缓存
缺点:依赖第三方库,且底层实现仍可能是遍历(需看具体库的优化)
缺点:依赖第三方库,且底层实现仍可能是遍历(需看具体库的优化)
总结
- 优先选 Map 缓存:适用于大多数场景,尤其是状态码不连续或有字符串编码的情况
- 次选数组映射:状态码是连续整数且范围较小时,性能最优
- 原生 valueOf ():仅适用于名称与枚举名一致的场景,简单直接
这些方法的核心都是通过 "空间换时间",避免每次映射时遍历枚举常量,从而提升性能。
如何在Java中使用自定义注解来实现枚举的双向映射?
在 Java 中,可以通过自定义注解结合反射机制实现枚举的双向映射,这种方式能让代码更具扩展性和复用性。下面是一个完整的实现方案:
实现思路
- 定义两个注解:
Code和Name,用于标记枚举的状态码和状态名称字段 - 创建一个工具类,通过反射解析注解,自动构建双向映射缓存
- 在枚举中使用注解标记对应字段,无需手动编写映射逻辑
使用自定义注解实现枚举双向映射
/////////////////////////////////////////Code.java
import java.lang.annotation.*;
// 该注解用于标记枚举中的状态码字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Code {
}
/////////////////////////////////////////Name.java
import java.lang.annotation.*;
// 该注解用于标记枚举中的状态名称字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
}
/////////////////////////////////////////EnumMappingUtils.java
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
public class EnumMappingUtils {
// 缓存:枚举类型 -> (code -> 枚举实例)
private static final Map<Class<? extends Enum<?>>, Map<Object, Enum<?>>> CODE_CACHE = new HashMap<>();
// 缓存:枚举类型 -> (name -> 枚举实例)
private static final Map<Class<? extends Enum<?>>, Map<Object, Enum<?>>> NAME_CACHE = new HashMap<>();
// 静态初始化指定枚举的缓存
public static <T extends Enum<?>> void init(Class<T> enumClass) {
// 如果已经初始化过,直接返回
if (CODE_CACHE.containsKey(enumClass)) {
return;
}
// 初始化两个映射表
Map<Object, Enum<?>> codeMap = new HashMap<>();
Map<Object, Enum<?>> nameMap = new HashMap<>();
// 反射获取枚举中的字段
Field codeField = findAnnotatedField(enumClass, Code.class);
Field nameField = findAnnotatedField(enumClass, Name.class);
if (codeField == null || nameField == null) {
throw new IllegalArgumentException("枚举类" + enumClass.getName() + "必须包含@Code和@Name注解的字段");
}
// 设置字段可访问(即使是private)
codeField.setAccessible(true);
nameField.setAccessible(true);
// 遍历所有枚举实例,填充缓存
for (T enumInstance : enumClass.getEnumConstants()) {
try {
Object code = codeField.get(enumInstance);
Object name = nameField.get(enumInstance);
codeMap.put(code, enumInstance);
nameMap.put(name, enumInstance);
} catch (IllegalAccessException e) {
throw new RuntimeException("解析枚举" + enumClass.getName() + "失败", e);
}
}
// 放入全局缓存
CODE_CACHE.put(enumClass, codeMap);
NAME_CACHE.put(enumClass, nameMap);
}
// 根据code获取枚举实例
public static <T extends Enum<?>> T getByCode(Class<T> enumClass, Object code) {
init(enumClass); // 确保已初始化
@SuppressWarnings("unchecked")
T result = (T) CODE_CACHE.get(enumClass).get(code);
if (result == null) {
throw new IllegalArgumentException("枚举" + enumClass.getName() + "不存在code为" + code + "的实例");
}
return result;
}
// 根据name获取枚举实例
public static <T extends Enum<?>> T getByName(Class<T> enumClass, Object name) {
init(enumClass); // 确保已初始化
@SuppressWarnings("unchecked")
T result = (T) NAME_CACHE.get(enumClass).get(name);
if (result == null) {
throw new IllegalArgumentException("枚举" + enumClass.getName() + "不存在name为" + name + "的实例");
}
return result;
}
// 查找带有指定注解的字段
private static Field findAnnotatedField(Class<?> enumClass, Class<? extends Annotation> annotationClass) {
for (Field field : enumClass.getDeclaredFields()) {
if (field.isAnnotationPresent(annotationClass)) {
return field;
}
}
return null;
}
}
/////////////////////////////////////////OrderStatus.java
// 订单状态枚举(使用自定义注解实现双向映射)
public enum OrderStatus {
PENDING(1, "待支付"),
PAID(2, "已支付"),
SHIPPED(3, "已发货"),
COMPLETED(4, "已完成"),
CANCELLED(5, "已取消"),
REFUNDING(6, "退款中"),
REFUNDED(7, "已退款");
// 用注解标记哪个字段是code,哪个是name
@Code
private final int code;
@Name
private final String name;
OrderStatus(int code, String name) {
this.code = code;
this.name = name;
}
// getter方法
public int getCode() {
return code;
}
public String getName() {
return name;
}
// 测试双向映射功能
public static void main(String[] args) {
// 从code获取枚举
OrderStatus status1 = EnumMappingUtils.getByCode(OrderStatus.class, 3);
System.out.println("通过code=3获取:" + status1); // 输出 SHIPPED
System.out.println("对应的name:" + status1.getName()); // 输出 已发货
// 从name获取枚举
OrderStatus status2 = EnumMappingUtils.getByName(OrderStatus.class, "已取消");
System.out.println("通过name=已取消获取:" + status2); // 输出 CANCELLED
System.out.println("对应的code:" + status2.getCode()); // 输出 5
}
}
实现说明
-
注解定义:
@Code:用于标记枚举中存储状态码的字段@Name:用于标记枚举中存储状态名称的字段- 两个注解都保留到运行时(
RetentionPolicy.RUNTIME),以便反射获取
-
工具类核心功能:
- 通过反射自动识别枚举中带有
@Code和@Name注解的字段 - 初始化时构建全局缓存,将 code 和 name 分别映射到对应的枚举实例
- 提供
getByCode()和getByName()方法供外部调用,实现双向映射
- 通过反射自动识别枚举中带有
-
使用方式:
- 在枚举的 code 字段上添加
@Code注解 - 在枚举的 name 字段上添加
@Name注解 - 无需编写任何映射逻辑,直接通过
EnumMappingUtils工具类进行双向查询
- 在枚举的 code 字段上添加
优点
- 代码复用:一套注解和工具类可用于所有需要双向映射的枚举,避免重复编码
- 低侵入性:枚举类只需添加注解,无需关心映射实现细节
- 易于维护:新增枚举常量时无需修改映射逻辑,只需正常定义即可
- 性能优化:通过缓存机制确保查询效率,时间复杂度为 O (1)
这种方式特别适合在大型项目中使用,当有多个枚举需要实现双向映射时,能显著减少代码量并提高一致性。
摘抄自网络,便于检索查找。

浙公网安备 33010602011771号