枚举类(enum)
本教程共 100 篇 · 第 50 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
本节目标:用
enum表达「固定几种取值」的场景(如星期、颜色、状态),理解它比老式int常量好在哪儿,并掌握枚举的字段、方法和比较方式。
表示「固定几种」最好的办法
很多数据天然只有有限个取值:星期只有周一到周日,颜色只有红绿蓝,订单状态只有待支付、已发货、已完成……
在 Java 5 之前,人们常用一组 int 常量来凑:
// 历史写法(不推荐)
class Status {
public static final int PENDING = 0;
public static final int SHIPPED = 1;
public static final int DONE = 2;
}
这种写法问题很多:传参时类型只是 int,你完全可以传个 99 进去,编译器不拦;而且字面意义弱、不能直接遍历所有可能。
Java 5 引入的 enum(枚举) 专门解决这个痛点。
用 enum 定义枚举
enum Status {
PENDING, SHIPPED, DONE
}
就这么简单。PENDING、SHIPPED、DONE 不是普通常量,而是 Status 类型的实例——而且实例个数在编译期就固定死了,运行时不可能冒出第四个。
使用起来类型安全:
Status s = Status.PENDING; // 类型就是 Status
// Status s2 = 99; // 编译错误,类型不对
Note枚举值习惯用大写,因为它们本质是
public static final的实例常量。枚举本身隐式继承自java.lang.Enum,所以不能再extends别的类(但可以实现接口)。
name() 和 ordinal()
每个枚举实例都有两个自带方法:
name():返回定义时的名字字符串,如"PENDING"。ordinal():返回它在定义中的序号(从 0 开始),如PENDING是 0、SHIPPED是 1。
Status s = Status.SHIPPED;
System.out.println(s.name()); // "SHIPPED"
System.out.println(s.ordinal()); // 1
Warning别把
ordinal()当成「业务序号」来用。一旦你调整枚举定义的顺序,所有ordinal()都会变,依赖它的逻辑就全乱了。需要业务编号时,请自定义字段(见下文),别依赖ordinal()。
枚举也能有字段和方法
枚举本质还是一个类,可以加字段、构造方法(必须是 private)和实例方法:
enum Status {
PENDING("待支付"),
SHIPPED("已发货"),
DONE("已完成");
private final String label; // 自定义字段
private Status(String label) { // 构造方法必须 private
this.label = label;
}
public String label() { // 取值方法
return label;
}
}
System.out.println(Status.SHIPPED.label()); // "已发货"
这样每个枚举值都携带了可读的中文说明,比光秃秃的名字友好得多。
在 switch 里用枚举
枚举和 switch 是天生一对,比 if-else 清晰,而且编译器能检查是否漏了分支:
Status s = Status.DONE;
switch (s) {
case PENDING -> System.out.println("还没付钱");
case SHIPPED -> System.out.println("在路上");
case DONE -> System.out.println("结束了");
}
这里用了 Java 14 转正的 switch 箭头语法(JEP 361),分支更紧凑、不用写 break。
Tip用枚举当
switch的表达式或判断对象,比用String或int更安全:编译器知道全部可能取值,你漏写一种它会提醒你。
枚举用 == 比较
枚举实例在 JVM 里是「单例」——每个枚举值全局只有一个对象。所以判断「是不是某个状态」直接用 == 即可,既正确又高效,不需要 equals:
if (s == Status.DONE) {
System.out.println("完成");
}
Note用
==比较枚举是推荐做法,因为它同时保证了「类型相同」和「同一个实例」,而且不会抛出空指针(比equals更稳)。
完整可运行示例
enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
public boolean isWorkday() {
return this != SAT && this != SUN;
}
}
public class Main {
public static void main(String[] args) {
Weekday d = Weekday.MON;
System.out.println(d); // MON
System.out.println(d.isWorkday()); // true
System.out.println(Weekday.SAT.isWorkday()); // false
}
}
values() 和 valueOf():遍历与反查
编译器给每个枚举偷偷加了几个静态方法:
values():返回所有枚举值的数组,方便遍历;valueOf(String):根据名字字符串反查出枚举值(名字不对会抛IllegalArgumentException)。
for (Status s : Status.values()) {
System.out.println(s + " -> " + s.label());
}
Status s = Status.valueOf("DONE"); // 按名字反查
这让枚举特别适合做「下拉选项」「配置映射」这类需要遍历、或按字符串还原的场景。
枚举可以实现接口
枚举也是类,可以实现接口,让每个枚举值共享一套能力:
interface Describable {
String desc();
}
enum Status implements Describable {
PENDING, SHIPPED, DONE;
public String desc() {
return switch (this) {
case PENDING -> "待支付";
case SHIPPED -> "已发货";
case DONE -> "已完成";
};
}
}
Tip如果所有枚举值的「描述」规则都一样,直接在枚举里统一实现接口方法即可;规则各不相同时,再考虑每个值各自覆盖。
枚举实现单例
因为枚举实例全局唯一、且 JVM 保证不会被反射重复创建,用枚举实现单例既简洁又安全,是《Effective Java》推荐的写法之一:
enum Singleton {
INSTANCE;
public void doWork() { }
}
调用方永远拿 Singleton.INSTANCE,不会窜出第二个实例。
EnumMap 与 EnumSet:给枚举定制的容器
用枚举当 key 时,别习惯性地掏 HashMap。JDK 在 java.util 里备了两个专用容器:
import java.util.*;
EnumMap<Status, Integer> counter = new EnumMap<>(Status.class);
counter.put(Status.PENDING, 3);
counter.put(Status.DONE, 10);
System.out.println(counter); // {PENDING=3, DONE=10}
EnumSet<Status> active = EnumSet.of(Status.PENDING, Status.SHIPPED);
System.out.println(active.contains(Status.DONE)); // false
它们快在哪:EnumMap 内部就是一个按 ordinal() 下标存取的数组,没有哈希计算、没有冲突处理;EnumSet 更极端,元素数不超过 64 个时用一个 long 的位来表示整个集合,判存在只是一次位运算。
顺带一个好处:EnumMap 和 EnumSet 的遍历顺序始终是枚举声明的顺序,不像 HashMap 那样看哈希值脸色。
Tip
EnumSet没有公开构造方法,只能靠静态工厂创建:of(...)列举、allOf(Status.class)全选、noneOf(Status.class)空集、range(A, B)取连续区间、complementOf(集合)取补集。
枚举的局限与适用边界
枚举虽好,也有边界:每个枚举值都是同类型的固定实例,不能在运行期动态新增。比如「订单状态」如果是产品写死的几种,用枚举完美;但若状态来自数据库、可能随时新增,就不能硬塞进枚举,而要改用普通类或查表。一句话:枚举只适合「编译期就确定的有限集合」,取值动态、无限的场景别用它。想清这一点,就不会用错地方。
常见疑问
valueOf() 传了个不存在的名字会怎样?
抛异常,而且是运行期才炸:
Exception in thread "main" java.lang.IllegalArgumentException:
No enum constant Status.DELETED
名字来自外部输入(表单、配置、接口参数)时,先兜住再用:
static Status parse(String text) {
for (Status s : Status.values()) {
if (s.name().equalsIgnoreCase(text)) return s;
}
return Status.PENDING; // 给个安全默认值
}
注意 valueOf 严格区分大小写,传 "done" 一样找不到。
为什么每次调 values() 拿到的数组都能随便改,却不影响枚举?
因为 values() 每次返回的都是新拷贝的数组,改它动不了枚举本身。代价是在循环里反复调会不停建数组,热点代码建议先存成一个私有静态常量再复用。
枚举能定义抽象方法吗?
能,而且每个枚举值各自实现,写法像匿名类:
enum Op {
ADD { public int apply(int a, int b) { return a + b; } },
MUL { public int apply(int a, int b) { return a * b; } };
public abstract int apply(int a, int b);
}
Op.MUL.apply(3, 4) 得到 12。这招适合「同一动作、每种取值算法不同」的场景。
小结
enum定义一组固定、有限的实例,编译器保证取值不会超出范围(Java 5 引入)。- 枚举自带
name()和ordinal(),但业务序号请用自定义字段,别依赖ordinal()。 - 枚举可以有字段、私有构造方法和实例方法,让每个取值携带更多信息。
- 枚举在
switch里特别好用,且直接用==比较即可。 - 替代了老式
int常量,类型安全、可读性好、可遍历。
下一章讲注解(Annotation),它是另一种「给代码贴标签」的机制,常被框架用来做配置和检查。