首页 / Java 入门教程 / 枚举类(enum)

Java 入门教程

枚举类(enum)

本教程共 100 篇 · 第 50 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程枚举enum常量switch

本节目标:用 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
}

就这么简单。PENDINGSHIPPEDDONE 不是普通常量,而是 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 的表达式或判断对象,比用 Stringint 更安全:编译器知道全部可能取值,你漏写一种它会提醒你。

枚举用 == 比较

枚举实例在 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 的位来表示整个集合,判存在只是一次位运算。

顺带一个好处:EnumMapEnumSet 的遍历顺序始终是枚举声明的顺序,不像 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),它是另一种「给代码贴标签」的机制,常被框架用来做配置和检查。