枚举
本篇定位
枚举(enum)是 JDK 5 引入的"有限实例集合"类型:一周七天、订单四种状态、HTTP 状态码分组——凡是"取值范围固定"的场景都该用它,而不是 int 常量或 String 魔法值。本篇按老规矩从问题出发:为什么不用常量?枚举底层是什么?单例为什么推荐枚举?
一、问题提出:常量方案的三大痛点
JDK 5 之前表示"季节",只能靠 int 常量或接口常量:
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;
}
三个致命问题:
- 类型不安全:
int season可以被赋成 99,编译器不吭声,运行时才炸; - 没有命名空间:
PRINTING = 1和SEASON_SPRING = 1打架,只能靠前缀区分; - 打印无意义:日志里打出
season=2,谁也不知道是夏是冬。
String 常量同样不安全(拼写错误静默通过)。枚举 = 编译器强制"取值只能在这几个里挑"。
二、基本使用:比想象中能干
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER // 分号可省略;后面还要写成员/方法时不能省
}
Season s = Season.SUMMER;
s.name(); // "SUMMER" —— 枚举常量的名字
s.ordinal(); // 1 —— 声明序号(从 0 开始)
s.toString(); // 默认同 name()
Season.valueOf("SPRING"); // 字符串 → 枚举,找不到抛 IllegalArgumentException
Season.values(); // 全部常量的数组(编译器合成方法)
枚举本质是类,所以可以有字段、构造器、方法——每个常量就是一份实例:
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。
抽象方法玩法:每个常量各自实现(编译器生成匿名子类)——
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 的字节码:
public final class Season extends java.lang.Enum<Season> {
public static final Season SPRING; // 每个"常量"就是一个 static final 实例
...
public static Season[] values();
public static Season valueOf(java.lang.String);
}
三个关键事实:
- enum 隐式继承
java.lang.Enum,所以枚举不能再 extends 别的类(Java 单继承被用掉了);可以实现接口; - 隐式 final(无抽象方法时),类被编译器锁死,外界无法继承、无法反射创建——
Constructor.newInstance()遇到枚举会直接抛IllegalArgumentException; values()/valueOf()是编译器合成的静态方法,在Enum父类里并不存在——这就是"在父类里找不到 values() 却能调用"的原因;每次调用values()都会克隆新数组,高频调用应缓存。
Enum 自带的 name()/ordinal()/compareTo()(按 ordinal 比较)/equals()(final,就是 ==)都是围绕"实例有限且唯一"展开的。
四、枚举实现单例:天然防反射、防序列化
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:
// EnumMap:内部是 Season.values().length 长度的数组,key.ordinal() 就是下标
Map<Season, Integer> count = new EnumMap<>(Season.class);
count.put(Season.SPRING, 3);
// EnumSet:long 位图实现(常量 ≤64 个时),add/contains 都是 O(1) 位运算
EnumSet<Season> warm = EnumSet.of(Season.SPRING, Season.SUMMER);
warm.contains(Season.SUMMER); // true
面试一句:key 是枚举时用 EnumMap 是白捡的性能优化(无装箱、无哈希冲突、无扩容)。
六、switch 支持枚举
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 字段 + 静态查找方法是最稳的落库姿势:
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. 枚举能常量化配置吗:枚举常量编译期固化,改取值要重新编译发版——频繁变化的配置别用枚举,那是数据库/配置中心的活。枚举表达的是"业务语义上就固定"的集合。
八、高频面试题速答
- 枚举和常量类的区别? 类型安全(非法值编译不过)、有命名空间、可带行为、天然单例;常量类三条全无。
- 枚举能继承类吗? 不能(已隐式继承 Enum);能实现接口,抽象方法各常量自行实现。
- 枚举单例为什么安全? JVM 禁止反射创建枚举实例 + 序列化由 JVM 保证唯一(对照第四节表格)。
- values() 是父类方法吗? 不是,编译器合成;且每次调用克隆新数组。
- EnumMap 为什么快? 数组下标 = ordinal,省掉哈希计算与冲突处理。
勾连
- 值传递与不可变设计:04-其他关键字
- 单例的其他写法与 volatile 语义:02-synchronized与volatile(DCL 一节)
- 枚举做 Map key 的容器选型:00-Map总览
- 注解为什么也"长得像枚举":03-注解原理
⬅️ 05-动态代理 🏠 00-Java ➡️ 07-Lambda与函数式接口
💬 评论