Java 枚举进阶:用带行为的 enum 消灭 switch-case,搭配 EnumMap 做策略分发
Java 枚举进阶:用带行为的 enum 消灭 switch-case,搭配 EnumMap 做策略分发你写过这样的代码吗:一个订单状态OrderStatus,每次要根据状态算折扣、发不同通知、判断能否退款,于是代码里散落着七八个switch (status)。加一个新状态,你得翻遍全项目找齐所有 switch,漏一个就是线上 bug。Java 的枚举其实远不止「一组常量」——它可以带字段、带方法、每个成员各自实现逻辑,把这些分散的判断收拢到一处。这篇讲怎么用它替掉恼人的 switch-case。朴素写法:switch 满天飞先看典型的「枚举 switch」写法,根据会员等级算折扣:enumLevel{BRONZE,SILVER,GOLD}classPriceService{doublediscount(Levellevel,doubleprice){switch(level){caseBRONZE:returnprice*0.98;caseSILVER:returnprice*0.95;caseGOLD:returnprice*0.90;default:thrownewIllegalArgumentException(未知等级);}}}问题不止一处 switch。等级相关的逻辑可能还有「积分倍率」「免邮门槛」,每个都得再写一个 switch。新增PLATINUM等级时,编译器不会提醒你哪些 switch 忘了改——default分支把漏写的都吞成了运行时异常。进阶:让枚举自己带字段和行为枚举成员本质是对象,可以有构造器、字段。先把「折扣率」这种数据塞进枚举本身:enumLevel{BRONZE(0.98),SILVER(0.95),GOLD(0.90);privatefinaldoublerate;// 每个成员携带自己的折扣率Level(doublerate){// 枚举构造器,天然 privatethis.raterate;}doubleapply(doubleprice){returnprice*rate;}}// 调用方彻底告别 switchdoublefinalPriceLevel.GOLD.apply(100);// 90.0数据和行为绑在成员上,apply一个方法搞定所有等级。但如果不同成员的逻辑不只是「乘个系数」,而是完全不同的算法呢?这就要用到更强的写法。每个成员各自实现:抽象方法 常量特定实现枚举可以声明抽象方法,由每个成员单独实现(constant-specific method body)。这相当于把 switch 的每个 case 变成一个成员的方法体,新增成员时编译器强制你实现方法,漏不掉:enumOperation{PLUS(){Overridedoubleapply(doublea,doubleb){returnab;}},MINUS(-){Overridedoubleapply(doublea,doubleb){returna-b;}},TIMES(*){Overridedoubleapply(doublea,doubleb){returna*b;}},DIVIDE(/){Overridedoubleapply(doublea,doubleb){if(b0)thrownewArithmeticException(除零);returna/b;}};privatefinalStringsymbol;Operation(Stringsymbol){this.symbolsymbol;}// 抽象方法:每个成员必须给出自己的实现abstractdoubleapply(doublea,doubleb);OverridepublicStringtoString(){returnsymbol;}}// 用法:遍历所有运算,天然覆盖全部成员publicstaticvoidmain(String[]args){doublex6,y2;for(Operationop:Operation.values()){System.out.printf(%.1f %s %.1f %.1f%n,x,op,y,op.apply(x,y));}}新增一个MOD(%)时,只要不实现apply,代码根本编译不过——这正是我们要的「加成员时编译器提醒」。逻辑内聚在各自成员里,调用方一行op.apply(x, y),再没有 switch。用 EnumMap 做策略分发:比 HashMap 更快更省有时候「行为」不适合塞进枚举本身(比如依赖 Spring 注入的 Service)。这时用EnumMap把枚举映射到处理器,是 switch 的另一种优雅替代:importjava.util.EnumMap;importjava.util.Map;importjava.util.function.Function;enumOrderStatus{CREATED,PAID,SHIPPED,DONE}classOrderNotifier{// EnumMap 底层是数组,按枚举 ordinal 索引,查找是 O(1) 且几乎零开销privatefinalMapOrderStatus,FunctionString,StringhandlersnewEnumMap(OrderStatus.class);OrderNotifier(){handlers.put(OrderStatus.CREATED,id-订单 id 已创建,待付款);handlers.put(OrderStatus.PAID,id-订单 id 已支付,备货中);handlers.put(OrderStatus.SHIPPED,id-订单 id 已发货);handlers.put(OrderStatus.DONE,id-订单 id 已完成);}Stringnotify(OrderStatusstatus,StringorderId){FunctionString,Stringhandlerhandlers.get(status);if(handlernull){thrownewIllegalStateException(未注册状态: status);}returnhandler.apply(orderId);}}为什么用EnumMap而不是HashMap?EnumMap内部就是一个数组,用枚举的ordinal()(声明顺序下标)直接定位,没有哈希计算、没有哈希冲突,查找和插入都比HashMap快,内存也更省。只要 key 是枚举,就用EnumMap。一个隐蔽的坑:别用 ordinal() 做持久化枚举有个ordinal()返回声明顺序(从 0 开始),很多人图省事拿它存数据库。这是定时炸弹:enumStatus{CREATED,PAID,DONE}// 存库时存了 ordinal:CREATED0, PAID1, DONE2// 某天有人在中间插入了一个新状态:enumStatus{CREATED,CANCELLED,PAID,DONE}// 现在 PAID 的 ordinal 从 1 变成 2,库里所有旧的 1 全被解释成了 CANCELLED正确做法是给枚举一个显式的、稳定的code 字段用于持久化,别依赖声明顺序:enumStatus{CREATED(1),PAID(2),DONE(3);privatefinalintcode;Status(intcode){this.codecode;}publicintgetCode(){returncode;}privatestaticfinalMapInteger,StatusBY_CODEnewHashMap();static{for(Statuss:values())BY_CODE.put(s.code,s);}publicstaticStatusfromCode(intcode){StatussBY_CODE.get(code);if(snull)thrownewIllegalArgumentException(非法 code: code);returns;}}code显式指定后,无论你怎么调整成员声明顺序,存进库的值都不会错位。小结枚举不是「常量集合」,它是能带字段、带方法的对象——把等级/状态相关的数据和逻辑收拢到成员上。逻辑各不相同时,用抽象方法 常量特定实现,新增成员编译器强制你补实现,消灭「漏改一处 switch」的隐患。行为依赖外部对象时,用EnumMap做策略分发:底层数组按ordinal索引,比HashMap更快更省。持久化绝不要用ordinal(),给枚举一个显式 code 字段,否则调整成员顺序会让历史数据全错位。一句话记忆:看到switch (枚举),先想想能不能让枚举自己干这活。