Appearance
第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 枚举无法比拟的。
💡 七、最佳实践
- 用枚举替代 int 常量:固定的一组值,枚举带来类型安全和可读性。
- 持久化用 name 或自定义 code,不用 ordinal。
- 枚举单例优于手写单例:线程安全、防反射、防反序列化。
- 行为多态用抽象方法/接口,而非 switch:新增值时编译器强制实现,避免遗漏。
- 大量枚举集合用 EnumSet/EnumMap:性能远超 HashSet/HashMap。
- 枚举值用
==比较:正确、高效、防 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 道题:
- 基本枚举定义与使用
- 带属性枚举(构造器、getter)
- 枚举 + switch
- EnumMap 统计
- 枚举单例
- 综合:用枚举实现状态机
完成后对比 答案/Sol11.java,查看逐行讲解与多解法。
🎓 下一步
- 第12课:常用类 — String 不可变与常量池、StringBuilder、包装类与缓存、java.time 日期时间