--- title: "06-枚举" created: 2026-09-02 tags: - Java --- # 枚举 > [!note] 本篇定位 > 枚举(enum)是 JDK 5 引入的"有限实例集合"类型:一周七天、订单四种状态、HTTP 状态码分组——凡是"取值范围固定"的场景都该用它,而不是 `int` 常量或 String 魔法值。本篇按老规矩从问题出发:为什么不用常量?枚举底层是什么?单例为什么推荐枚举? ## 一、问题提出:常量方案的三大痛点 JDK 5 之前表示"季节",只能靠 `int` 常量或接口常量: ```java public class SeasonConstant { public static final int SPRING = 1; public static final int SUMMER = 2; public static final int AUTUMN = 3; public static final int WINTER = 4; } ``` 三个致命问题: 1. **类型不安全**:`int season` 可以被赋成 99,编译器不吭声,运行时才炸; 2. **没有命名空间**:`PRINTING = 1` 和 `SEASON_SPRING = 1` 打架,只能靠前缀区分; 3. **打印无意义**:日志里打出 `season=2`,谁也不知道是夏是冬。 String 常量同样不安全(拼写错误静默通过)。**枚举 = 编译器强制"取值只能在这几个里挑"**。 ## 二、基本使用:比想象中能干 ```java public enum Season { SPRING, SUMMER, AUTUMN, WINTER // 分号可省略;后面还要写成员/方法时不能省 } ``` ```java Season s = Season.SUMMER; s.name(); // "SUMMER" —— 枚举常量的名字 s.ordinal(); // 1 —— 声明序号(从 0 开始) s.toString(); // 默认同 name() Season.valueOf("SPRING"); // 字符串 → 枚举,找不到抛 IllegalArgumentException Season.values(); // 全部常量的数组(编译器合成方法) ``` 枚举本质是类,所以**可以有字段、构造器、方法**——每个常量就是一份实例: ```java public enum Season { SPRING("春", "温暖"), SUMMER("夏", "炎热"), AUTUMN("秋", "凉爽"), WINTER("冬", "寒冷"); // 常量列表以分号结束 private final String cnName; private final String desc; Season(String cnName, String desc) { // 构造器天然 private this.cnName = cnName; this.desc = desc; } public String getCnName() { return cnName; } public String getDesc() { return desc; } } ``` 规则:构造器只能是 private(编译器强制,反正外界也不该 new);常量必须在第一行列出;字段建议 final。 **抽象方法玩法**:每个常量各自实现(编译器生成匿名子类)—— ```java public enum Operation { PLUS { public int apply(int a, int b) { return a + b; } }, MINUS { public int apply(int a, int b) { return a - b; } }; public abstract int apply(int a, int b); } Operation.PLUS.apply(1, 2); // 3 ``` ## 三、底层原理:枚举是"被驯化的类" 用 `javap` 看一眼 `Season` 的字节码: ```java public final class Season extends java.lang.Enum { public static final Season SPRING; // 每个"常量"就是一个 static final 实例 ... public static Season[] values(); public static Season valueOf(java.lang.String); } ``` 三个关键事实: 1. **enum 隐式继承 `java.lang.Enum`**,所以枚举不能再 extends 别的类(Java 单继承被用掉了);可以实现接口; 2. **隐式 final**(无抽象方法时),类被编译器锁死,外界无法继承、无法反射创建——`Constructor.newInstance()` 遇到枚举会直接抛 `IllegalArgumentException`; 3. `values()` / `valueOf()` 是**编译器合成的静态方法**,在 `Enum` 父类里并不存在——这就是"在父类里找不到 values() 却能调用"的原因;每次调用 `values()` 都会克隆新数组,高频调用应缓存。 `Enum` 自带的 `name()`/`ordinal()`/`compareTo()`(按 ordinal 比较)/`equals()`(final,就是 `==`)都是围绕"实例有限且唯一"展开的。 ## 四、枚举实现单例:天然防反射、防序列化 ```java public enum Singleton { INSTANCE; private int counter; public void doSomething() { counter++; } } ``` 调用:`Singleton.INSTANCE.doSomething()`。 为什么《Effective Java》力荐枚举单例: | 攻击面 | 普通单例(DCL/静态内部类) | 枚举单例 | | --- | --- | --- | | 反射创建 | 需要构造器抛异常防御 | JVM 层面直接禁止,反射抛异常 | | 序列化破坏 | 需重写 `readResolve()` 防止反序列化出新实例 | JVM 保证同一枚举常量反序列化后仍是同一实例 | | 写法复杂度 | 双重检查锁一坨模板代码 | 一行 | 枚举单例唯一的"缺点":不能懒加载(类加载即初始化)——但对绝大多数场景这根本不是问题。 ## 五、与集合的配合:EnumMap / EnumSet 当枚举做 key / 元素时,JDK 提供了专为枚举优化的集合——内部就是数组+位图,无哈希计算,性能碾压 HashMap/HashSet: ```java // EnumMap:内部是 Season.values().length 长度的数组,key.ordinal() 就是下标 Map count = new EnumMap<>(Season.class); count.put(Season.SPRING, 3); // EnumSet:long 位图实现(常量 ≤64 个时),add/contains 都是 O(1) 位运算 EnumSet warm = EnumSet.of(Season.SPRING, Season.SUMMER); warm.contains(Season.SUMMER); // true ``` 面试一句:**key 是枚举时用 EnumMap 是白捡的性能优化**(无装箱、无哈希冲突、无扩容)。 ## 六、switch 支持枚举 ```java switch (s) { case SPRING -> desc = "回暖"; // JDK 14+ 箭头语法;case 直接写常量名,不加枚举类前缀 case SUMMER -> desc = "炎热"; default -> desc = "其他"; } ``` switch 对枚举的支持从 JDK 5 就有,编译器按 ordinal 跳表;JDK 21 的 switch 模式匹配进一步支持枚举与模式组合(新特性速览篇展开)。 ## 七、实战与坑 **1. 用枚举替代 if-else 分支**(策略味道):状态机、权限点、支付渠道,每种取值一个行为实现,加状态只加枚举常量不改主干(开闭原则的轻量版)。 **2. 数据库存取**:MyBatis/JPA 都支持枚举映射——注意存 `name()` 还是 `ordinal()` 的选择:**ordinal 会因调整常量顺序而错位,永远优先存 name 或 code**。给枚举加个 `code` 字段 + 静态查找方法是最稳的落库姿势: ```java public enum OrderStatus { PAID(1), SHIPPED(2), DONE(3); private final int code; OrderStatus(int code) { this.code = code; } public static OrderStatus of(int code) { for (OrderStatus s : values()) if (s.code == code) return s; throw new IllegalArgumentException("未知状态: " + code); } } ``` **3. values() 高频调用坑**:每次返回克隆数组,写在超热循环里是无谓开销,缓存成 `private static final List` 即可。 **4. 枚举能常量化配置吗**:枚举常量编译期固化,改取值要重新编译发版——**频繁变化的配置别用枚举**,那是数据库/配置中心的活。枚举表达的是"业务语义上就固定"的集合。 ## 八、高频面试题速答 1. **枚举和常量类的区别?** 类型安全(非法值编译不过)、有命名空间、可带行为、天然单例;常量类三条全无。 2. **枚举能继承类吗?** 不能(已隐式继承 Enum);能实现接口,抽象方法各常量自行实现。 3. **枚举单例为什么安全?** JVM 禁止反射创建枚举实例 + 序列化由 JVM 保证唯一(对照第四节表格)。 4. **values() 是父类方法吗?** 不是,编译器合成;且每次调用克隆新数组。 5. **EnumMap 为什么快?** 数组下标 = ordinal,省掉哈希计算与冲突处理。 ## 勾连 - 值传递与不可变设计:[[04-其他关键字|04-其他关键字]] - 单例的其他写法与 volatile 语义:[[02-synchronized与volatile|02-synchronized与volatile]](DCL 一节) - 枚举做 Map key 的容器选型:[[00-Map总览|00-Map总览]] - 注解为什么也"长得像枚举":[[03-注解原理|03-注解原理]] --- ⬅️ [[05-动态代理|05-动态代理]] 🏠 [[00-Java|00-Java]] ➡️ [[07-Lambda与函数式接口|07-Lambda与函数式接口]]