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.rate=rate;}doubleapply(doubleprice){returnprice*rate;}}// 调用方彻底告别 switchdoublefinalPrice=Level.GOLD.apply(100);// 90.0数据和行为绑在成员上,apply一个方法搞定所有等级。但如果不同成员的逻辑不只是「乘个系数」,而是完全不同的算法呢?这就要用到更强的写法。
每个成员各自实现:抽象方法 + 常量特定实现
枚举可以声明抽象方法,由每个成员单独实现(constant-specific method body)。这相当于把 switch 的每个 case 变成一个成员的方法体,新增成员时编译器强制你实现方法,漏不掉:
enumOperation{PLUS("+"){@Overridedoubleapply(doublea,doubleb){returna+b;}},MINUS("-"){@Overridedoubleapply(doublea,doubleb){returna-b;}},TIMES("*"){@Overridedoubleapply(doublea,doubleb){returna*b;}},DIVIDE("/"){@Overridedoubleapply(doublea,doubleb){if(b==0)thrownewArithmeticException("除零");returna/b;}};privatefinalStringsymbol;Operation(Stringsymbol){this.symbol=symbol;}// 抽象方法:每个成员必须给出自己的实现abstractdoubleapply(doublea,doubleb);@OverridepublicStringtoString(){returnsymbol;}}// 用法:遍历所有运算,天然覆盖全部成员publicstaticvoidmain(String[]args){doublex=6,y=2;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) 且几乎零开销privatefinalMap<OrderStatus,Function<String,String>>handlers=newEnumMap<>(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){Function<String,String>handler=handlers.get(status);if(handler==null){thrownewIllegalStateException("未注册状态: "+status);}returnhandler.apply(orderId);}}为什么用EnumMap而不是HashMap?EnumMap内部就是一个数组,用枚举的ordinal()(声明顺序下标)直接定位,没有哈希计算、没有哈希冲突,查找和插入都比HashMap快,内存也更省。只要 key 是枚举,就用EnumMap。
一个隐蔽的坑:别用 ordinal() 做持久化
枚举有个ordinal()返回声明顺序(从 0 开始),很多人图省事拿它存数据库。这是定时炸弹:
enumStatus{CREATED,PAID,DONE}// 存库时存了 ordinal:CREATED=0, PAID=1, DONE=2// 某天有人在中间插入了一个新状态:enumStatus{CREATED,CANCELLED,PAID,DONE}// 现在 PAID 的 ordinal 从 1 变成 2,库里所有旧的 "1" 全被解释成了 CANCELLED正确做法是给枚举一个显式的、稳定的code 字段用于持久化,别依赖声明顺序:
enumStatus{CREATED(1),PAID(2),DONE(3);privatefinalintcode;Status(intcode){this.code=code;}publicintgetCode(){returncode;}privatestaticfinalMap<Integer,Status>BY_CODE=newHashMap<>();static{for(Statuss:values())BY_CODE.put(s.code,s);}publicstaticStatusfromCode(intcode){Statuss=BY_CODE.get(code);if(s==null)thrownewIllegalArgumentException("非法 code: "+code);returns;}}code显式指定后,无论你怎么调整成员声明顺序,存进库的值都不会错位。
小结
- 枚举不是「常量集合」,它是能带字段、带方法的对象——把等级/状态相关的数据和逻辑收拢到成员上。
- 逻辑各不相同时,用抽象方法 + 常量特定实现,新增成员编译器强制你补实现,消灭「漏改一处 switch」的隐患。
- 行为依赖外部对象时,用
EnumMap做策略分发:底层数组按ordinal索引,比HashMap更快更省。 - 持久化绝不要用
ordinal(),给枚举一个显式 code 字段,否则调整成员顺序会让历史数据全错位。 - 一句话记忆:看到
switch (枚举),先想想能不能让枚举自己干这活。