Skip to content

第11课:枚举

🎯 学习目标

  • 理解枚举的本质(它是继承自 java.lang.Enum 的 final 类,不是简单的常量)
  • 掌握枚举的定义、带属性构造、枚举方法(values/valueOf/ordinal/name)
  • 掌握枚举与 switch、EnumSet、EnumMap 的使用
  • 理解枚举实现接口与抽象方法(策略模式)、枚举单例
  • 识别枚举的常见陷阱(ordinal 不稳定、不能被继承、switch 漏 case)

📖 一、概念讲解:枚举的本质

1. 为什么需要枚举

传统用 public static final int 表示一组固定值,有两个致命问题:

java
// 传统方式
public class Season {
    public static final int SPRING = 1;
    public static final int SUMMER = 2;
}

int season = 100;           // ❌ 没有任何约束,可以塞任何 int
if (season == Season.SPRING) { ... }   // 类型不安全,调试困难

枚举解决「类型安全」与「封闭集合」问题:

java
public enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}
Season s = Season.SPRING;   // ✅ 只能是这 4 个之一

2. 枚举的本质

关键认知:Java 的 enum 不是 C 那样的整型常量集合,而是真正的类。每个枚举值都是该类的一个 public static final 实例。

java
public enum Season { SPRING, SUMMER, AUTUMN, WINTER }

等价于(伪代码):

java
// 编译器生成的类:继承 java.lang.Enum,是 final 类
public final class Season extends java.lang.Enum<Season> {
    public static final Season SPRING = new Season("SPRING", 0);
    public static final Season SUMMER = new Season("SUMMER", 1);
    public static final Season AUTUMN = new Season("AUTUMN", 2);
    public static final Season WINTER = new Season("WINTER", 3);
    private Season(String name, int ordinal) { super(name, ordinal); }
}

结论:枚举是类 → 可以有字段、方法、构造器、实现接口;但 final → 不能被继承;构造器只能 private → 外部不能 new,实例封闭在定义的几个值内。


📖 二、枚举定义

1. 带属性的枚举

java
public enum Season {
    SPRING("春天", "温暖"),
    SUMMER("夏天", "炎热"),
    AUTUMN("秋天", "凉爽"),
    WINTER("冬天", "寒冷");

    private final String name;        // 建议用 final
    private final String description;

    // 构造方法必须是 private(默认就是 private,写不写都行,但不能写 public/protected)
    private Season(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public String getName() { return name; }
    public String getDescription() { return description; }
}

// 使用
Season s = Season.SPRING;
System.out.println(s.getName() + " - " + s.getDescription());   // 春天 - 温暖

关键点:枚举值列表(SPRING(...),...)必须在最前面,用 ; 结尾,之后才是字段和方法。构造器只能是 private——这保证了实例的封闭性。


📖 三、枚举自带方法

java
Season[] all = Season.values();          // 所有枚举值数组
Season s = Season.valueOf("SPRING");     // 按名称转枚举(非法名抛 IllegalArgumentException)
String name = Season.SPRING.name();      // "SPRING"(名称)
int ord = Season.SPRING.ordinal();       // 0(声明顺序,从0开始)

// 比较推荐用 ==(枚举是单例,同一值就是同一对象)
if (s == Season.SPRING) { ... }          // ✅ 推荐
if (s.equals(Season.SPRING)) { ... }    // ✅ 也可以,但啰嗦

// 枚举实现了 Comparable,可比较 ordinal
Season.SPRING.compareTo(Season.WINTER); // 负数(0 < 3)

枚举中定义抽象方法(策略模式)

java
public enum Operation {
    PLUS  { public double apply(double x, double y) { return x + y; } },
    MINUS { public double apply(double x, double y) { return x - y; } },
    TIMES { public double apply(double x, double y) { return x * y; } };

    // 抽象方法:每个枚举值必须实现
    public abstract double apply(double x, double y);
}

double r = Operation.PLUS.apply(10, 5);   // 15.0

原理:定义抽象方法的枚举,编译器会为每个枚举值生成一个匿名子类(如 Operation$1)。所以 Operation.PLUS 实际是 Operation 子类的实例——这是枚举能实现「每个值不同行为」的机制。


📖 四、枚举应用

1. switch

java
public String getMessage(Season s) {
    switch (s) {
        case SPRING: return "春暖花开";
        case SUMMER: return "夏日炎炎";
        case AUTUMN: return "秋高气爽";
        case WINTER: return "冬雪纷飞";
        default:     return "未知";   // 即使覆盖了所有值,IDE 也建议保留 default 防新增
    }
}

2. EnumSet / EnumMap(高性能集合)

java
import java.util.EnumSet;
import java.util.EnumMap;

// EnumSet:用位向量实现,极快,适合标志位组合
EnumSet<Season> warm = EnumSet.of(Season.SPRING, Season.SUMMER);
System.out.println(warm.contains(Season.SUMMER));   // true

// EnumMap:内部用数组,key 为枚举的 ordinal,O(1) 访问
EnumMap<Season, String> msg = new EnumMap<>(Season.class);
msg.put(Season.SPRING, "春");
msg.get(Season.SPRING);   // "春"

为什么快:EnumSet/EnumMap 直接用 ordinal 作数组下标,省去 hash 计算和冲突处理,性能优于 HashSet/HashMap。

3. 枚举单例(Effective Java 推荐)

java
public enum Singleton {
    INSTANCE;
    public void doSomething() { System.out.println("单例"); }
}
// 使用:Singleton.INSTANCE.doSomething();

为什么枚举单例最佳:天然线程安全(JVM 类加载保证)、防反射攻击(枚举构造特殊,反射 newInstance 会抛异常)、防反序列化破坏(枚举序列化机制特殊)。比双重检查锁、静态内部类等实现更简洁可靠。

4. 枚举实现接口

java
public interface Describable { String describe(); }

public enum Color implements Describable {
    RED   { public String describe() { return "红色"; } },
    GREEN { public String describe() { return "绿色"; } };
}

⚠️ 五、常见陷阱

陷阱1:ordinal 不稳定,勿用于持久化

java
public enum Status { ACTIVE, INACTIVE, DELETED }
// 若顺序改为 ACTIVE, DELETED, INACTIVE,ordinal 变化,数据库里存的 ordinal 全错
int ord = Status.ACTIVE.ordinal();   // 依赖声明顺序,不稳定

正确做法:持久化用 name()(字符串)或自定义稳定 code 字段,绝不依赖 ordinal。

陷阱2:枚举不能被继承

java
public enum Base { A, B }
// class Sub extends Base { }   // ❌ 枚举是 final,不能继承

变通:枚举可实现接口来扩展行为(见上「枚举实现接口」)。枚举值本身是匿名子类,能各自 override 方法。

陷阱3:switch 漏 case / 新增值未处理

新增枚举值后,所有 switch 若无 default 会静默漏处理。建议保留 default,或用枚举的抽象方法代替 switch(每新增一个值就必须实现方法,编译器强制覆盖)。

陷阱4:valueOf 抛异常

java
Season s = Season.valueOf("Spring");   // ❌ 抛 IllegalArgumentException(大小写敏感)

变通:自己写一个安全的 fromName,遍历 values() 忽略大小写匹配。

陷阱5:枚举值间用 == 比较 vs equals

java
Season.SPRING == Season.SPRING;          // ✅ 推荐(单例,同一对象)
Season.SPRING.equals(Season.SPRING);     // ✅ 也对但啰嗦

枚举每个值是单例,== 既正确又高效,还能避免 NPE(== 对 null 不抛异常,equals 若左值为 null 会 NPE)。


🆚 六、Java vs C 对比

特性C 语言Java
枚举本质int 常量集合(enum { A, B }; 实际是 int)真正的 final 类,每个值是单例对象
类型安全弱(int x = A; x = 100; 合法)强(只能赋枚举值)
带属性/方法不支持(只是整数)支持字段、构造器、方法、抽象方法
继承枚举是 final 不可继承,但可实现接口
单例手写(static + 锁)enum Singleton { INSTANCE; } 一行搞定且线程安全
内存4 字节 int对象引用

对 C 程序员:把 Java 枚举理解为「一个自动生成 N 个单例对象、且封闭实例的类」,而不是「命名的整数」。它能携带数据和行为,是 C 枚举无法比拟的。


💡 七、最佳实践

  1. 用枚举替代 int 常量:固定的一组值,枚举带来类型安全和可读性。
  2. 持久化用 name 或自定义 code,不用 ordinal
  3. 枚举单例优于手写单例:线程安全、防反射、防反序列化。
  4. 行为多态用抽象方法/接口,而非 switch:新增值时编译器强制实现,避免遗漏。
  5. 大量枚举集合用 EnumSet/EnumMap:性能远超 HashSet/HashMap。
  6. 枚举值用 == 比较:正确、高效、防 NPE。

📖 八、稳定 code 字段

实际项目中,枚举经常需要和数据库、前端、配置文件交互。

推荐定义稳定 code:

java
public enum OrderStatus {
    CREATED("created", "已创建"),
    PAID("paid", "已支付"),
    CANCELLED("cancelled", "已取消");

    private final String code;
    private final String label;

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

    public String code() {
        return code;
    }

    public String label() {
        return label;
    }
}

数据库可以保存 code,而不是 ordinal


🧪 九、安全的 fromCode

不要直接使用 valueOf 处理外部输入。

java
public static Optional<OrderStatus> fromCode(String code) {
    for (OrderStatus status : values()) {
        if (status.code.equals(code)) {
            return Optional.of(status);
        }
    }
    return Optional.empty();
}

如果业务要求非法值直接报错:

java
public static OrderStatus requireByCode(String code) {
    return fromCode(code)
        .orElseThrow(() -> new IllegalArgumentException("unknown status: " + code));
}

外部输入可能为空、大小写不同或版本不匹配。用 fromCode 能把错误集中处理。


🧪 十、枚举状态机

枚举可以表达状态流转。

java
public enum OrderStatus {
    CREATED {
        public boolean canTransferTo(OrderStatus next) {
            return next == PAID || next == CANCELLED;
        }
    },
    PAID {
        public boolean canTransferTo(OrderStatus next) {
            return next == FINISHED;
        }
    },
    CANCELLED,
    FINISHED;

    public boolean canTransferTo(OrderStatus next) {
        return false;
    }
}

使用:

java
if (!current.canTransferTo(next)) {
    throw new IllegalStateException("illegal status transfer");
}

优点:

text
状态规则靠近状态本身。
新增状态时容易发现需要补规则。
比散落在业务代码里的 if-else 更集中。

📊 十一、枚举使用场景

适合枚举:

text
订单状态。
用户角色。
支付渠道。
任务类型。
错误码类别。
固定配置选项。
策略集合。

不适合枚举:

text
运行时动态增加的值。
数据库中经常变化的字典项。
需要用户自定义的分类。
数量很大且频繁变化的列表。

判断标准:

text
如果值集合由代码版本控制,适合枚举。
如果值集合由运营或用户动态维护,更适合数据库字典表。

🛠 十二、枚举排查清单

常见问题:

text
数据库保存 ordinal,新增枚举后数据错乱。
valueOf 处理外部输入导致异常。
switch 新增枚举后漏处理。
枚举字段不是 final,导致状态可变。
枚举承担太多业务逻辑,变成上帝类。
前后端枚举 code 不一致。

建议:

text
持久化保存稳定 code。
提供 fromCode 方法。
字段尽量 final。
行为复杂时考虑策略类。
对外接口明确返回 code 还是 name。

✅ 十三、掌握标准

学完本课后,应能做到:

text
能解释 Java 枚举是类,不是 int。
能定义带字段和构造器的枚举。
能使用 values、valueOf、name、ordinal。
能说明为什么不应持久化 ordinal。
能使用 EnumSet 和 EnumMap。
能用枚举实现简单策略和状态机。
能写 fromCode 处理外部输入。
能判断枚举和数据库字典表的边界。

枚举的核心价值是把固定集合变成类型安全的对象。它不只是常量,更可以承载稳定的小规模行为。


📝 练习预告

完成 练习/Ex11_Enum.java 中的 6 道题:

  1. 基本枚举定义与使用
  2. 带属性枚举(构造器、getter)
  3. 枚举 + switch
  4. EnumMap 统计
  5. 枚举单例
  6. 综合:用枚举实现状态机

完成后对比 答案/Sol11.java,查看逐行讲解与多解法。


🎓 下一步

  • 第12课:常用类 — String 不可变与常量池、StringBuilder、包装类与缓存、java.time 日期时间