1. 项目概述:为什么我们需要深入理解Java枚举?
在Java开发中,枚举(Enum)是一个看似简单,实则内涵丰富的类型。很多开发者,尤其是刚入行的朋友,常常把它当作一个“高级常量列表”来用,定义几个值就完事了。但在我十多年的项目实战和面试官经历中,我发现对枚举的深入理解和灵活运用,往往是区分普通程序员和资深工程师的一道分水岭。无论是处理状态机、策略模式,还是构建清晰、安全的领域模型,枚举都扮演着至关重要的角色。
这个项目的核心,就是彻底搞懂Java枚举的“来龙去脉”。我们不仅要会定义枚举,更要能像操作普通对象一样,动态地获取枚举对象、遍历枚举列表、提取枚举值,甚至实现更复杂的查找和映射逻辑。这不仅仅是应付“Java面试八股文”里的几个问题,更是为了在实际编码中写出更健壮、更易维护、更具表达力的代码。想象一下,当你需要根据一个字符串动态获取对应的枚举实例,或者需要将枚举的所有值展示给前端时,如果只会写死代码,那将是一场维护噩梦。接下来,我将带你从基础到进阶,手把手拆解Java枚举的完整操作手册。
2. 枚举基础回顾与核心设计思想
在深入各种获取操作之前,我们必须先统一认知:Java中的枚举究竟是什么?它绝不仅仅是public static final的语法糖。从JVM层面看,每个枚举类型都隐式继承自java.lang.Enum类,这意味着枚举是类(Class),而每个枚举常量都是这个类的单例实例(Instance)。理解这一点,是后续一切灵活操作的前提。
2.1 枚举的本质:一个有实例限制的类
当你写下enum Status { PENDING, PROCESSING, SUCCESS, FAILED }时,编译器实际上为你生成了一个类似下面的类:
final class Status extends Enum<Status> { public static final Status PENDING = new Status("PENDING", 0); public static final Status PROCESSING = new Status("PROCESSING", 1); public static final Status SUCCESS = new Status("SUCCESS", 2); public static final Status FAILED = new Status("FAILED", 3); private static final Status[] $VALUES = new Status[]{PENDING, PROCESSING, SUCCESS, FAILED}; private Status(String name, int ordinal) { super(name, ordinal); } public static Status[] values() { return $VALUES.clone(); } public static Status valueOf(String name) { // ... 查找逻辑 } }可以看到,每个枚举常量都是一个static final的实例,并且在静态代码块中被初始化。数组$VALUES(对应公开的values()方法)保存了所有实例的有序列表。这个设计带来了几个关键特性:类型安全(无法创建枚举定义之外的实例)、单例性(每个常量全局唯一)、可遍历性(通过values()获取所有实例)。我们的所有获取操作,都是基于这个底层模型展开的。
2.2 定义包含属性的枚举
在实际项目中,我们很少使用“光秃秃”的枚举。更常见的做法是为枚举附加业务属性,使其成为一个丰富的值对象。例如,一个标准的订单状态枚举可能会这样定义:
public enum OrderStatus { // 枚举实例(构造参数: 状态码, 描述) UNPAID(10, "待支付"), PAID(20, "已支付"), DELIVERING(30, "配送中"), RECEIVED(40, "已收货"), CLOSED(50, "交易关闭"); private final int code; private final String desc; // 枚举的构造器必须是私有的 OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } public int getCode() { return code; } public String getDesc() { return desc; } }这里,code和desc就是枚举的“状态”。这种设计模式使得枚举不仅能表示状态,还能携带与之相关的元数据,极大地提升了代码的表达能力和数据一致性。后续我们根据code或desc反向查找枚举对象,就是基于这样的结构。
实操心得:将枚举的属性字段声明为
final(如上例中的private final int code)是一个非常好的实践。这能强制保证枚举实例的不可变性(Immutable),而不可变对象是线程安全的,可以放心地在多线程环境中共享,无需担心状态被意外修改。这是利用枚举实现线程安全状态机的关键之一。
3. 核心操作一:获取单个枚举对象
获取单个枚举对象是最常见、最核心的需求。通常,我们手头有一个标识(比如字符串名称、数字编码),需要将其转换为对应的、类型安全的枚举实例。Java提供了内置方法,但在复杂场景下我们需要自己扩展。
3.1 基础方法:Enum.valueOf()与枚举自身的valueOf()
这是最直接的获取方式,适用于通过枚举常量的精确名称进行查找。
// 使用 java.lang.Enum 的静态方法,需要传入枚举的 Class 对象 Status status1 = Enum.valueOf(Status.class, "SUCCESS"); // 更常用的方式是直接使用枚举类型自身的 valueOf() 方法(编译器生成) Status status2 = Status.valueOf("SUCCESS");关键点与避坑指南:
- 大小写敏感:传入的字符串必须与枚举常量声明的名称完全一致,包括大小写。
Status.valueOf(“success”)会抛出IllegalArgumentException。 - 性能考量:
valueOf()方法内部通常会遍历$VALUES数组进行匹配。对于频繁调用的热点代码,如果枚举实例很多,可以考虑使用Map进行缓存优化(下文会讲)。 - 空值处理:如果传入的字符串为
null,或者找不到对应的常量,都会抛出IllegalArgumentException。在生产代码中,强烈建议对其进行封装,提供更友好的异常处理或返回默认值。
public static Status getByStatusSafe(String statusStr) { if (statusStr == null) { return null; // 或 return Status.UNKNOWN; } try { return Status.valueOf(statusStr.toUpperCase()); // 统一转大写处理 } catch (IllegalArgumentException e) { log.warn(“无效的状态值: {}”, statusStr); return null; // 或 return Status.UNKNOWN; } }3.2 进阶方法:根据自定义属性查找枚举对象
当我们需要根据业务编码(如code)或描述(desc)来查找枚举时,就需要自定义静态查找方法。这是体现枚举灵活性的关键。
方案一:静态Map缓存(推荐)这是性能最优、最标准的做法。在枚举内部维护一个静态的Map,在类加载时初始化,实现 O(1) 时间复杂度的查找。
public enum OrderStatus { UNPAID(10, “待支付”), PAID(20, “已支付”), // ... 其他实例 private final int code; private final String desc; // 静态缓存Map private static final Map<Integer, OrderStatus> CODE_MAP = new HashMap<>(); private static final Map<String, OrderStatus> DESC_MAP = new HashMap<>(); // 静态初始化块,用于构建缓存 static { for (OrderStatus status : values()) { CODE_MAP.put(status.code, status); DESC_MAP.put(status.desc, status); } } OrderStatus(int code, String desc) { this.code = code; this.desc = desc; } // 根据code查找 public static OrderStatus fromCode(Integer code) { if (code == null) { return null; // 或抛出异常 } return CODE_MAP.get(code); } // 根据desc查找 public static OrderStatus fromDesc(String desc) { if (desc == null || desc.isEmpty()) { return null; } return DESC_MAP.get(desc); } // ... getters }为什么推荐Map缓存?
- 性能:枚举的
values()方法每次调用都会返回一个新数组(clone),如果每次查找都遍历数组,在频繁调用或枚举项多时会产生不必要的性能开销和垃圾回收压力。静态Map只需初始化一次,查找是常数时间。 - 清晰:将查找逻辑封装在枚举内部,符合“高内聚”原则。外部调用方只需
OrderStatus.fromCode(10)即可,无需关心内部实现。
方案二:遍历查找(简单场景)如果枚举项非常少(比如少于10个),且查找操作不频繁,为了代码极简,也可以直接遍历。
public static OrderStatus fromCodeByLoop(int code) { for (OrderStatus status : OrderStatus.values()) { if (status.getCode() == code) { return status; } } throw new IllegalArgumentException(“无效的状态码: ” + code); }注意事项:使用
Map缓存时,请确保作为key的属性(如code,desc)在枚举定义中是唯一的。如果有重复的code,后放入Map的实例会覆盖前者,导致查找结果错误。在静态初始化块中添加assert或日志检查是一个好习惯。
3.3 处理查找失败:异常 vs. 默认值 vs. Optional
当查找失败时,如何处理是设计API时需要仔细考虑的。
- 抛出受检异常:强制调用方处理,适合“找不到就是严重错误”的场景。
- 返回
null:简单,但容易导致后续的NullPointerException,调用方必须做空值判断。 - 返回默认枚举实例:如
OrderStatus.UNKNOWN,适合有明确兜底逻辑的场景。 - 返回
Optional(Java 8+):最现代、最安全的方式,明确告知调用方结果可能为空,必须处理。
// 使用Optional的示例 public static Optional<OrderStatus> fromCodeSafely(Integer code) { return Optional.ofNullable(CODE_MAP.get(code)); } // 调用方 OrderStatus.fromCodeSafely(someCode) .ifPresentOrElse( status -> process(status), () -> log.warn(“状态码未找到”) );4. 核心操作二:获取枚举值(属性)
获取枚举值,指的是获取枚举实例所关联的业务属性,如code,desc等。这通常很简单,直接通过getter方法即可。
OrderStatus status = OrderStatus.PAID; int statusCode = status.getCode(); // 20 String statusDesc = status.getDesc(); // “已支付”但这里有一个更深层次的技巧:让枚举实现接口。这可以让我们以统一的方式处理不同类型的枚举,获取其“值”。
假设我们有一个通用需求:将各种枚举转换成前端下拉框需要的{label, value}格式。我们可以定义一个LabelValueEnum接口:
public interface LabelValueEnum { String getLabel(); // 对应 desc Object getValue(); // 对应 code, 用Object以适应不同类型 }然后让业务枚举实现这个接口:
public enum OrderStatus implements LabelValueEnum { UNPAID(10, “待支付”), // ... 其他; private final int code; private final String desc; @Override public String getLabel() { return this.desc; } @Override public Object getValue() { return this.code; // 自动装箱为Integer } // ... }这样,我们就可以编写通用的工具方法,处理所有实现了LabelValueEnum的枚举:
public static List<Map<String, Object>> toLabelValueList(Class<? extends LabelValueEnum> enumClass) { LabelValueEnum[] enums = enumClass.getEnumConstants(); return Arrays.stream(enums) .map(e -> { Map<String, Object> map = new HashMap<>(); map.put(“label”, e.getLabel()); map.put(“value”, e.getValue()); return map; }) .collect(Collectors.toList()); } // 调用 List<Map<String, Object>> options = toLabelValueList(OrderStatus.class);这种基于接口的设计,极大地提高了代码的复用性和扩展性,是应对复杂业务系统中多种枚举处理的优雅方案。
5. 核心操作三:获取枚举列表(遍历与转换)
获取枚举的所有实例列表是最基本的遍历操作,但围绕这个列表,我们可以做很多有用的转换和聚合。
5.1 基础遍历:values()方法
每个枚举类型都有一个由编译器生成的public static T[] values()方法,它返回包含所有枚举常量的数组,顺序与声明顺序一致。
// 1. 直接遍历 for (OrderStatus status : OrderStatus.values()) { System.out.println(status.name() + “: ” + status.getDesc()); } // 2. 使用Stream API (Java 8+) List<String> descList = Arrays.stream(OrderStatus.values()) .map(OrderStatus::getDesc) .collect(Collectors.toList()); // 3. 转换为Set(常用于快速存在性判断) Set<OrderStatus> statusSet = EnumSet.allOf(OrderStatus.class);重要提醒:values()方法每次调用都会返回一个新的数组(克隆)。在性能极其敏感或循环次数极多的场景下,可以考虑将其结果缓存到局部变量或静态字段中,避免重复创建数组。但对于绝大多数应用,这点开销可以忽略不计。
5.2 使用EnumSet和EnumMap进行高效操作
Java集合框架为枚举提供了两个高性能的专用实现:EnumSet和EnumMap。它们在内部使用位向量或数组,性能远超通用的HashSet和HashMap。
EnumSet:枚举集合
- 场景:需要表示一个枚举类型的子集,或进行集合运算(如并集、交集)。
- 示例:定义一组需要特殊处理的“终态”。
// 定义一个“终态”集合 EnumSet<OrderStatus> finalStatusSet = EnumSet.of(OrderStatus.RECEIVED, OrderStatus.CLOSED); // 判断某个状态是否为终态 if (finalStatusSet.contains(currentStatus)) { // 进行终态后的处理,如发送通知、更新统计 } // 获取所有非终态 EnumSet<OrderStatus> nonFinalStatusSet = EnumSet.complementOf(finalStatusSet);EnumMap:以枚举为键的Map
- 场景:需要根据枚举常量关联其他对象或配置。
- 示例:为不同状态配置不同的处理器。
// 定义状态处理器映射 EnumMap<OrderStatus, StatusHandler> handlerMap = new EnumMap<>(OrderStatus.class); handlerMap.put(OrderStatus.UNPAID, new UnpaidHandler()); handlerMap.put(OrderStatus.PAID, new PaidHandler()); // ... // 根据状态获取处理器 StatusHandler handler = handlerMap.get(currentStatus); if (handler != null) { handler.handle(order); }使用EnumMap不仅速度快,而且键是类型安全的,不可能放入不属于该枚举类型的键。
5.3 复杂转换:将枚举列表转换为各种数据结构
在实际开发中,我们经常需要将枚举列表转换为前端、接口或配置需要的格式。
1. 转换为List<DTO>这是前后端交互中最常见的场景。
public class StatusDTO { private String key; // 枚举名,如 “PAID” private String label; // 描述,如 “已支付” private Integer value; // 编码,如 20 // 构造器、getter/setter省略 } public static List<StatusDTO> toDTOList() { return Arrays.stream(OrderStatus.values()) .map(status -> new StatusDTO(status.name(), status.getDesc(), status.getCode())) .collect(Collectors.toList()); }2. 转换为Map方便根据某个属性进行快速查找。虽然我们有静态的CODE_MAP,但有时可能需要临时的、不同key的映射。
// 转换为 Map<code, desc> Map<Integer, String> codeToDescMap = Arrays.stream(OrderStatus.values()) .collect(Collectors.toMap(OrderStatus::getCode, OrderStatus::getDesc)); // 转换为 Map<name, enum> Map<String, OrderStatus> nameToEnumMap = Arrays.stream(OrderStatus.values()) .collect(Collectors.toMap(Enum::name, Function.identity()));3. 按条件分组例如,将所有状态按是否可操作进行分组。
// 假设有一个方法判断状态是否可操作 Map<Boolean, List<OrderStatus>> groupedByOperable = Arrays.stream(OrderStatus.values()) .collect(Collectors.groupingBy(OrderStatus::isOperable));6. 实战应用场景与模式
掌握了基本操作,我们来看看枚举在真实项目中的高级玩法。这些模式能显著提升代码质量。
6.1 状态机(State Machine)
枚举是实现轻量级状态机的绝佳载体。我们可以将状态转移逻辑封装在枚举内部。
public enum OrderStatus { UNPAID { @Override public OrderStatus next() { return PAID; // 待支付的下一个状态是已支付 } }, PAID { @Override public OrderStatus next() { return DELIVERING; } }, DELIVERING { @Override public OrderStatus next() { return RECEIVED; } }, RECEIVED, CLOSED; // 终态没有next方法 // 定义抽象方法,强制每个非终态枚举实现状态转移逻辑 public OrderStatus next() { throw new IllegalStateException(“状态 [” + this.name() + “] 是终态,无法转移”); } // 一个实用的方法:判断是否能转移到目标状态 public boolean canTransferTo(OrderStatus target) { // 这里可以定义更复杂的规则,比如根据订单类型、用户权限等判断 // 简单示例:只允许按预定义顺序转移 return this.next() == target; } } // 使用 OrderStatus current = OrderStatus.UNPAID; if (current.canTransferTo(OrderStatus.PAID)) { current = current.next(); // 更新订单状态到数据库 }这种设计将状态转移规则集中管理,避免了业务代码中散布的if-else判断,使状态流转清晰可见且易于维护。
6.2 策略模式(Strategy Pattern)
当不同枚举值需要对应不同的行为时,可以使用策略模式。
public enum NotificationChannel { SMS { @Override public void send(String message, String target) { // 调用短信网关SDK smsGateway.send(target, message); } }, EMAIL { @Override public void send(String message, String target) { // 构造并发送邮件 emailService.sendHtmlMail(target, “通知”, message); } }, WECHAT { @Override public void send(String message, String target) { // 调用微信模板消息接口 wechatTemplateService.send(target, message); } }; // 抽象策略方法 public abstract void send(String message, String target); } // 调用方代码变得极其简洁 String channel = getUserPreferredChannel(); NotificationChannel.valueOf(channel.toUpperCase()) .send(“您的订单已发货”, user.getPhone());6.3 配置化与国际化
枚举值需要展示给用户,其描述(desc)可能需要支持多语言。我们可以将描述信息外置到属性文件或数据库中。
方案:枚举与资源文件绑定
- 为每个枚举定义一个唯一的消息键(
messageKey)。 - 在枚举的
getDesc()方法中,根据messageKey去资源文件(如messages.properties)中获取对应的国际化描述。
public enum OrderStatus { UNPAID(“order.status.unpaid”), PAID(“order.status.paid”); // ... private final String messageKey; public String getDesc() { // ResourceBundleUtil是自定义的获取国际化资源的工具类 return ResourceBundleUtil.getMessage(this.messageKey); } }这样,当系统切换语言时,枚举的描述会自动跟随变化,实现了枚举的国际化。
7. 常见问题、性能陷阱与排查技巧
即使对枚举很熟悉,在实际开发中还是会遇到一些坑。这里记录几个典型问题和解决方案。
7.1values()方法的内存与性能影响
如前所述,values()返回新数组。在超高并发或极度追求性能的场景(如高频交易的金融核心系统),在循环中反复调用values()可能成为瓶颈。
优化方案:
public enum OrderStatus { // ... 常量定义 private static final OrderStatus[] VALUES_CACHE = values(); // 缓存数组 public static OrderStatus[] getValues() { return VALUES_CACHE.clone(); // 返回克隆,保护内部数组 } public static List<OrderStatus> getValueList() { return Collections.unmodifiableList(Arrays.asList(VALUES_CACHE)); // 返回不可变列表 } }通过一个静态的缓存数组,避免每次调用values()都执行clone()。对外暴露时,依然返回克隆或不可变集合,以保证枚举数组本身不会被外部修改。
7.2 序列化与反序列化问题
在使用JSON序列化框架(如Jackson、Gson)或进行RPC调用时,枚举的序列化方式需要关注。
默认行为:大多数框架默认序列化枚举的name()(字符串名称)。这通常没问题。反序列化风险:框架默认使用Enum.valueOf()根据字符串反序列化。如果接收到的字符串不存在(如大小写问题、前后空格),会抛出异常。
解决方案(以Jackson为例):
使用
@JsonCreator和@JsonValue注解,指定序列化/反序列化的属性。public enum OrderStatus { UNPAID(10, “待支付”); // ... @JsonValue // 序列化时,使用code作为值 public int getCode() { return this.code; } @JsonCreator // 反序列化时,根据code查找枚举 public static OrderStatus fromCode(int code) { // 调用我们之前写的 fromCode 方法 OrderStatus status = CODE_MAP.get(code); if (status == null) { throw new IllegalArgumentException(“无效状态码: ” + code); } return status; } }这样,JSON中传输的是数字
10,而不是字符串“UNPAID”,更节省空间,且对前端友好。配置全局的枚举反序列化策略,使其更宽容(如忽略大小写)。
ObjectMapper mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true); // 未知枚举值转为null mapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true); // 使用默认值
7.3 枚举在数据库中的存储
如何将枚举存入数据库?通常有三种方式:
- 存储
name()(String):VARCHAR类型,可读性好,但占用空间稍大,且如果重构枚举改名,需要同步更新数据库。 - 存储
ordinal()(int):TINYINT/INT类型,节省空间,但强烈不推荐。因为ordinal()依赖于枚举常量的声明顺序,一旦在枚举中间插入新的常量,所有顺序都会改变,导致数据错乱。 - 存储自定义
code(int):TINYINT/INT类型,推荐。使用我们自定义的、稳定的业务编码,与声明顺序解耦。持久化框架(如JPA/Hibernate)可以通过@Converter自动完成枚举与code的转换。
JPA存储枚举示例:
@Converter(autoApply = true) public class OrderStatusConverter implements AttributeConverter<OrderStatus, Integer> { @Override public Integer convertToDatabaseColumn(OrderStatus status) { return status != null ? status.getCode() : null; } @Override public OrderStatus convertToEntityAttribute(Integer code) { return code != null ? OrderStatus.fromCode(code) : null; } } @Entity public class Order { @Convert(converter = OrderStatusConverter.class) private OrderStatus status; }7.4 枚举的线程安全性与单例
枚举实例是static final的,由JVM在类加载时初始化,且构造器私有,这天然保证了枚举常量的线程安全性和单例性。你可以放心地在多线程环境中使用==来比较枚举常量,这是完全安全的。这也是为什么用枚举实现单例模式(Effective Java 推荐)是比双重检查锁定等方式更简洁、安全的选择。
public enum Singleton { INSTANCE; private SomeResource resource; Singleton() { // 初始化资源,此构造器仅被JVM调用一次 this.resource = new SomeResource(); } public SomeResource getResource() { return resource; } } // 使用 Singleton.INSTANCE.getResource().doSomething();8. 高级技巧与扩展思考
8.1 使用枚举实现职责链(Chain of Responsibility)
对于有顺序的流程处理,可以将处理器与枚举结合。
public enum FileProcessor { VALIDATE { @Override public boolean process(FileContext context) { // 验证逻辑 if (!valid) return false; return next().process(context); } }, PARSE { @Override public boolean process(FileContext context) { // 解析逻辑 return next().process(context); } }, PERSIST { @Override public boolean process(FileContext context) { // 持久化逻辑 return true; // 最后一个处理器 } }; public abstract boolean process(FileContext context); public FileProcessor next() { // 获取下一个处理器,最后一个处理器的next返回自身或null int nextOrdinal = this.ordinal() + 1; FileProcessor[] processors = values(); return nextOrdinal < processors.length ? processors[nextOrdinal] : null; } // 启动入口 public static void processFile(FileContext context) { VALIDATE.process(context); } }8.2 枚举与Spring框架的集成
在Spring项目中,我们经常需要将枚举作为Bean的属性注入,或者让枚举实现Spring的接口。
将枚举作为Bean注入:通常不需要,因为枚举本身就是单例。但你可以通过@Component注解一个配置类,来提供复杂的枚举工厂方法。
在Thymeleaf或Freemarker中遍历枚举:可以直接将Enum.values()传递到视图层,或者通过工具类生成List<LabelValue>。
在Spring MVC中接收枚举参数:Controller方法可以直接使用枚举类型接收参数,Spring会尝试用valueOf()进行转换。为了更健壮,可以配合自定义的Converter或PropertyEditor。
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { // 注册一个全局的String到OrderStatus的转换器 registry.addConverter(String.class, OrderStatus.class, source -> { try { // 先尝试按name转换 return OrderStatus.valueOf(source.toUpperCase()); } catch (IllegalArgumentException e1) { try { // 再尝试按code转换 return OrderStatus.fromCode(Integer.parseInt(source)); } catch (NumberFormatException e2) { throw new IllegalArgumentException(“无法将 [” + source + “] 转换为OrderStatus”); } } }); } } // Controller中 @GetMapping(“/order”) public String listOrders(@RequestParam OrderStatus status) { // Spring会自动转换 // ... }8.3 枚举的“扩展”问题
一个经典的局限是:枚举在编译时就必须确定所有实例,无法在运行时动态添加。如果遇到需要动态“类型”的场景,可以考虑以下替代方案:
- 使用普通类+注册表:定义一个接口或抽象类,然后用一个
Map来管理其实例。这提供了运行时动态注册的能力,但失去了枚举的类型安全和编译器检查。 - 使用枚举+外部配置:将可变部分(如描述、关联的类名)提取到数据库或配置文件中,枚举值本身固定,但行为或属性可配置。
例如,对于“通知渠道”,渠道类型(SMS, EMAIL)是固定的,但每个渠道的具体配置(模板ID、密钥)可以放在数据库里。枚举负责定义类型和获取处理器,处理器再从数据库加载配置执行。
经过以上从基础到进阶的系统性拆解,你会发现Java枚举远不止一个常量列表。它结合了类的强大表达能力、实例的严格可控性以及数组的便捷遍历性。真正掌握获取枚举对象、值和列表的种种技巧,并灵活运用状态机、策略等模式,能让你设计出的代码更加简洁、健壮和优雅。下次在代码中定义枚举时,不妨多思考一下:这个枚举需要根据什么属性查找?它的所有值需要以什么形式展示?它未来会不会有复杂的行为?想清楚这些问题,你写出的就不仅仅是代码,而是经过精心设计的产品模块了。