CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 进阶/课时/枚举与模式
RawBlame
课时目录12 节

Java 进阶

  1. 01Java 进阶概览
  2. 02枚举与模式
  3. 03Set/Queue/Stack
  4. 04OOP 进阶
  5. 05方法链与绑定
  6. 06Record 与不可变
  7. 07模块系统
  8. 08并发进阶
  9. 09依赖注入概念
  10. 10Java Web 框架
  11. 11Quarkus 入门
  12. 12综合练习

枚举与模式

enums、switch 表达式。

30 分钟

枚举与模式

这一课你将能做到

  1. 定义带字段、构造器与业务方法的 enum(如订单状态机),并用 switch 表达式写出强制穷尽的分支逻辑
  2. 用 EnumSet / EnumMap 高效存储枚举集合,说清它们比 HashSet/HashMap 快在哪
  3. 避开 ordinal 持久化、values() 性能、switch 漏分支、null 比较 四类高频坑

为什么要学

订单状态、支付渠道、课时发布状态……任何「取值有限」的字段,用字符串存就是运行时炸弹:拼写错误编译器不管,"paid" 与 "PAID" 混用靠人肉排查,脏数据直接写进 MySQL。enum 把这组值升级成类型——写错即编译报错,IDE 自动补全所有合法值,数据库里存的也是显式编码。

面试角度:枚举单例为什么线程安全、枚举比较用 == 还是 equals、EnumMap 原理,都是中高频题;「设计一下订单状态流转」更是业务设计题的常见开局。本课答案按能背的标准写。

先建立图像

String 状态:   "paid"   "PAID"   "Paid"   "payd"    ← 编译器:都合法,祝你好运
enum 状态:     OrderStatus.PAID                    ← 编译器:只此一个,写错即红

enum 本质是继承 java.lang.Enum 的 final 类,每个常量是它的一个静态单例实例:

// 编译器眼中,enum WAIT_PAY, PAID 大致等价于:
public final class OrderStatus extends Enum<OrderStatus> {
    public static final OrderStatus WAIT_PAY = new OrderStatus("WAIT_PAY", 0);
    public static final OrderStatus PAID     = new OrderStatus("PAID", 1);
    // name 与 ordinal 由父类字段持有
}

跟着做

步骤 1:定义带行为的订单状态 enum

新建 EnumDemo.java,完整可运行:

public class EnumDemo {

    public enum OrderStatus {
        WAIT_PAY(10, "待支付"),
        PAID(20, "已支付"),
        SHIPPED(30, "已发货"),
        REFUNDED(40, "已退款");

        private final int code;     // 存数据库的显式编码
        private final String desc;  // 给前端展示的文案

        OrderStatus(int code, String desc) {
            this.code = code;
            this.desc = desc;
        }

        public int code() { return code; }
        public String desc() { return desc; }

        /** 是否已付过钱:发货、退款都建立在已支付之上 */
        public boolean paid() {
            return this == PAID || this == SHIPPED || this == REFUNDED;
        }

        /** 由数据库编码反查枚举;查不到直接抛异常,绝不让脏数据溜过去 */
        public static OrderStatus of(int code) {
            for (OrderStatus s : values()) {
                if (s.code == code) return s;
            }
            throw new IllegalArgumentException("未知订单状态编码: " + code);
        }
    }

    public static void main(String[] args) {
        OrderStatus s = OrderStatus.of(20);
        System.out.println(s + " -> " + s.desc() + ", 已付款=" + s.paid());

        System.out.println(s == OrderStatus.PAID);          // true,枚举比较用 ==
        System.out.println(s.equals(OrderStatus.PAID));     // true,但没必要
        try {
            OrderStatus.of(99);
        } catch (IllegalArgumentException e) {
            System.out.println("拦截脏数据: " + e.getMessage());
        }
    }
}

运行:java EnumDemo.java。

步骤 2:加入 switch 表达式,验证强制穷尽

在 EnumDemo 里加入以下方法,并把 main 换成调用它的版本:

    static String nextAction(OrderStatus status) {
        return switch (status) {
            case WAIT_PAY -> "发送催付通知";
            case PAID     -> "通知仓库发货";
            case SHIPPED  -> "等待用户签收";
            case REFUNDED -> "关闭工单";
        };
    }

    public static void main(String[] args) {
        for (OrderStatus s : OrderStatus.values()) {
            System.out.println(s + " -> " + nextAction(s));
        }
    }

然后注释掉 case SHIPPED 一行再编译——直接失败,提示 switch 表达式未覆盖所有输入值。这就是「漏分支在编译期暴露」,if-else 链永远做不到。

步骤 3:EnumSet 与 EnumMap

再在 main 前面加上 import 与演示代码(import 放文件头部):

import java.util.EnumMap;
import java.util.EnumSet;
    EnumSet<OrderStatus> visible = EnumSet.of(OrderStatus.WAIT_PAY, OrderStatus.PAID);
    EnumMap<OrderStatus, Integer> count = new EnumMap<>(OrderStatus.class);
    count.put(OrderStatus.PAID, 3);
    System.out.println(visible.contains(OrderStatus.PAID)); // true
    System.out.println(count);                              // {PAID=3}

步骤 4(选做):JUnit 验证状态机

把 OrderStatus 提为独立文件 OrderStatus.java(去掉 public enum 前的缩进层级不变),测试类放同包下即可运行 mvn test:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OrderStatusTest {

    @Test
    void ofShouldRejectUnknownCode() {
        assertThrows(IllegalArgumentException.class, () -> OrderStatus.of(99));
    }

    @Test
    void refundedShouldCountAsPaid() {
        assertTrue(OrderStatus.REFUNDED.paid());
    }

    @Test
    void switchShouldCoverAllValues() {
        for (OrderStatus s : OrderStatus.values()) {
            assertNotNull(EnumDemo.nextAction(s));
        }
    }
}

讲清楚要点

  1. 每个枚举常量是单例:JVM 类加载时创建且全局唯一,== 比较既正确又比 equals 快,还是天然的 NPE 防护(null == 常量 直接 false)。枚举是少数鼓励用 == 的场景。
  2. 构造器天然私有:enum 构造器只能是 private(不写修饰符也是),外部无法 new OrderStatus(...),取值集合封闭可控。
  3. switch 表达式三要素:箭头语法无 fall-through(不需要 break);对枚举必须穷尽所有常量(或写 default);它有返回值,可直接赋给变量。
  4. 别把 ordinal 存进数据库:ordinal 跟着声明顺序走,中间插入一个新常量,历史数据全部错位。持久化用显式 code 字段,反查用 of(code) 工厂。
  5. EnumSet / EnumMap 是枚举专用高性能容器:EnumSet 底层是位向量(long 数组按 ordinal 置位),EnumMap 底层是按 ordinal 直接下标的数组——没有哈希计算与冲突处理,且元素按声明顺序排列。
  6. 值集封闭才用 enum:会动态增长的集合(用户自定义标签、运营可配置分类)用字典表 + String + 校验,不要用 enum 硬扛。

你可能会懵的地方

  • enum 能继承别的类吗? 不能,它已隐式继承 java.lang.Enum;但可以实现接口。进阶玩法是「常量级多态」:enum 实现抽象方法,每个常量提供自己的实现。
  • values() 每次调用有什么代价? 内部数组会被 clone 后返回新数组,高频调用产生分配压力;循环外缓存结果,或用 EnumSet.allOf(...) 替代。
  • 为什么枚举能做线程安全的单例? 实例在类加载时由 JVM 创建且只创建一次;反射调用枚举构造器会被 JVM 直接抛 IllegalArgumentException;序列化由 JVM 特殊处理,反序列化不会生成新实例。防反射、防序列化破坏,写法还最短。
  • 数据库里的编码和枚举怎么双向映射? 显式 code 字段 + 静态 of(code) 工厂;Spring 项目里用 JPA 的 @Enumerated(EnumType.STRING) 或 MyBatis 的 TypeHandler,思想相同:用名字或显式编码,绝不用 ordinal。
  • 一个状态可以同时是两个值吗? 单个字段不行——那是状态机设计错误。一个订单既有「已支付」又有「已开票」这类正交维度时,拆成两个独立字段或两个 enum,不要往一个枚举里塞组合态。

错法对照

错法问题正解
全链路传 "PAID" 字符串拼写错误编译期无感知业务层传 enum,序列化边界再转字符串
把 ordinal 存库或当对外编码新增常量导致历史数据错位定义显式 code 字段 + of() 反查
switch 用旧语句式且漏 breakfall-through 静默串分支箭头语法 switch 表达式
对可能为 null 的枚举调 equalsNPE用 == 比较
运营要加标签就改 enum每次改代码重发版字典表 + String + 校验

卡点急救

  1. 编译报「the switch expression does not cover all possible input values」:枚举分支没写全。补齐所有常量;确实允许「未知值」时加 default,但要评估是否应该把未知值挡在入口。
  2. of(code) 返回 null 后到处 NPE:工厂方法查不到就抛 IllegalArgumentException,不要返回 null 把炸弹往后传。
  3. 枚举字段在运行时被改:字段一律 private final,只提供访问器;发现需要可变状态,说明这个概念不该建模成 enum。

面试常问

  • 为什么说枚举单例是最佳实践? 可背版:① 类加载机制保证实例唯一且线程安全,无需 double-checked locking;② JVM 拦截反射创建枚举实例;③ 序列化机制对枚举特殊处理,反序列化不产生新实例。三条正好封死单例被破坏的常见路径。
  • EnumMap 为什么比 HashMap 快? 内部按 ordinal 作为数组下标直接定位,无哈希函数计算、无冲突链表/红黑树,空间连续对 CPU 缓存友好;代价是只能装同一种枚举的键。
  • switch 表达式与 switch 语句的区别? 表达式有值、可赋值;箭头语法无 fall-through;对枚举强制穷尽。语句只执行分支不产值、易漏 break。新代码一律用表达式,老代码重构优先级也高。

自测三问

  1. 为什么枚举字段要 private final?构造器为什么不写 private 也禁止外部调用?
  2. 订单状态编码存进数据库,用 ordinal 有什么风险?正确做法是什么?
  3. EnumSet.of(A, B).contains(x) 的底层复杂度是多少?为什么?

小结一句话

enum 把「有限取值」升级成编译期可检查的类型,配合 switch 表达式的强制穷尽,业务状态机从此不靠人肉谨慎。

延伸

  • java.lang.Enum 官方文档
  • 下一课:Set / Queue / Stack
加载本节练习…
← 上一课Java 进阶概览下一课 →Set/Queue/Stack