主题
界面
主题
枚举解决的问题很具体:当一个值的取值只有有限的几种时,如何保证不会传错。
它常被当成"几个常量的集合"而低估——实际上枚举可以带字段、带方法、实现接口,是 Java 里"用类型表达业务规则"最顺手的工具。
完成本节后,你应该能够:
values()、valueOf()、name()、ordinal();== 比较;switch 表达式处理枚举,并知道编译器会帮你检查遗漏;EnumMap 做以枚举为键的统计;int 常量代替枚举"的两个具体风险。switch 表达式吗?(《运算符与流程控制》)先看没有枚举时的写法:
public class OrderStatus {
public static final int CREATED = 0;
public static final int PAID = 1;
public static final int SHIPPED = 2;
}
void updateStatus(int status) { ... }
updateStatus(99); // 编译通过!运行时才发现"未知状态"
updateStatus(PAID); // 看起来对,但打印出来是 1,日志里看不懂两个问题很致命:
int 能表示 21 亿个值,其中绝大多数是非法状态,编译器帮不上忙;1,排查问题时得回去翻常量定义。枚举的第一行就把这两个问题一起解决:
public enum OrderStatus { CREATED, PAID, SHIPPED }public enum OrderStatus {
CREATED, PAID, SHIPPED, COMPLETED, CANCELED
}枚举常量实际上是 public static final 的单例对象——这一点决定了后面所有特性。
public enum OrderStatus {
CREATED("已创建", true),
PAID("已支付", true),
SHIPPED("已发货", false);
private final String label;
private final boolean cancelable;
OrderStatus(String label, boolean cancelable) { // 构造方法只能是 private
this.label = label;
this.cancelable = cancelable;
}
public String label() { return label; }
public boolean cancelable() { return cancelable; }
}关键点:
private(写不写都是 private),因为常量是固定的那几个,外部不能 new;Map 里。| 方法 | 作用 | 注意 |
|---|---|---|
values() | 返回所有常量的数组 | 每次调用都会新建数组(有轻微开销) |
valueOf("PAID") | 按名称还原 | 名称不存在抛 IllegalArgumentException |
name() | 返回常量名 | 与 toString() 的区别:name() 不可重写 |
ordinal() | 返回序号(从 0 开始) | 不要用于持久化,见下文 |
ordinal 的陷阱
ordinal() 是常量在声明列表中的位置。一旦有人调整了声明顺序,或者插入了新常量,所有历史数据里的序号就全错了。
需要持久化时,用 name() 或自定义的业务编码,不要用 ordinal()。
OrderStatus status = OrderStatus.PAID;
status == OrderStatus.PAID; // 推荐:枚举是单例,== 安全且更快
status.equals(OrderStatus.PAID); // 也行,但没必要
String hint = switch (status) { // switch 表达式 + 枚举
case CREATED -> "等待支付";
case PAID -> "等待发货";
case SHIPPED -> "等待收货";
case COMPLETED, CANCELED -> "流程结束";
};switch 与枚举配合时,编译器会检查是否覆盖了全部常量——如果新增一个枚举常量却忘了在 switch 里处理,编译就会失败(表达式形式没有任何 default 时)。这是一道非常有价值的编译期防线。
它们是专为枚举设计的高效容器:
Map<OrderStatus, Integer> counter = new EnumMap<>(OrderStatus.class);
counter.merge(OrderStatus.PAID, 1, Integer::sum);优势:内部用数组实现,比 HashMap 更快更省内存,且遍历顺序就是枚举的声明顺序(可预测)。
EnumSet 同理,适合表达"一组权限"这类场景:EnumSet.of(READ, WRITE)。
public interface Describable {
String describe();
}
public enum OrderStatus implements Describable {
CREATED("已创建") { // 每个常量可以有自己的实现
@Override
public String describe() { return "订单刚创建,等待支付"; }
},
PAID("已支付") {
@Override
public String describe() { return "已收到款项"; }
};
// ...
}这种"每个常量一个实现"的写法叫常量特定方法体,适合规则差异较大的场景。但要注意:常量多时代码会变得很长,多数情况下带字段 + switch 更易读。
enum Season { SPRING, SUMMER, AUTUMN, WINTER }
for (Season season : Season.values()) {
System.out.println(season.name() + " / " + season.ordinal());
}public boolean canTransitionTo(OrderStatus next) {
return switch (this) {
case CREATED -> next == PAID || next == CANCELED;
case PAID -> next == SHIPPED || next == CANCELED;
case SHIPPED -> next == COMPLETED;
case COMPLETED, CANCELED -> false;
};
}把"允许的流转"写进枚举,好处是规则只有一处:任何地方要判断状态能否变更,都调这个方法,而不是各写一套 if。
if ("PAID".equals(status)) { ... } // 拼写错误编译器不管
if ("paid".equals(status)) { ... } // 大小写不一致,逻辑走不进去
order.setStatus("已支付"); // 中文、英文混着来,没人拦得住枚举的写法:
if (status == OrderStatus.PAID) { ... } // 拼错编译不过
order.setStatus(OrderStatus.PAID); // 只能传合法值判断标准:如果一个字段的取值是"人定的、有限的几个"(状态、类型、级别、渠道),就该用枚举。
文件:labs/oop-basics/src/main/java/cn/kb/learn/oop/OrderStatus.java 与 EnumLesson.java
package cn.kb.learn.oop;
import java.util.Arrays;
public enum OrderStatus {
CREATED("已创建", true),
PAID("已支付", true),
SHIPPED("已发货", false),
COMPLETED("已完成", false),
CANCELED("已取消", false);
private final String label;
private final boolean cancelable;
OrderStatus(String label, boolean cancelable) {
this.label = label;
this.cancelable = cancelable;
}
public String label() {
return label;
}
public boolean cancelable() {
return cancelable;
}
/** 允许的状态流转 */
public boolean canTransitionTo(OrderStatus next) {
return switch (this) {
case CREATED -> next == PAID || next == CANCELED;
case PAID -> next == SHIPPED || next == CANCELED;
case SHIPPED -> next == COMPLETED;
case COMPLETED, CANCELED -> false;
};
}
public static OrderStatus fromLabel(String label) {
return Arrays.stream(values())
.filter(status -> status.label.equals(label))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未知状态: " + label));
}
}运行命令:
cd labs
./mvnw -pl oop-basics compile
java -cp oop-basics/target/classes cn.kb.learn.oop.EnumLesson预期输出:
全部状态: [CREATED, PAID, SHIPPED, COMPLETED, CANCELED]
name = PAID,ordinal = 1
是同一常量: true
中文名: 已支付
是否可取消: true
valueOf: SHIPPED
按中文名查找: SHIPPED
CREATED → PAID: true
COMPLETED → PAID: false
状态统计: {CREATED=1, PAID=2, SHIPPED=1, CANCELED=1}
已创建 的下一步: 等待支付
已发货 的下一步: 等待收货
已取消 的下一步: 流程结束注意倒数第 4 行:EnumMap 的输出顺序是 CREATED → PAID → SHIPPED → CANCELED,与枚举声明顺序一致——这正是 EnumMap 相比 HashMap 的额外好处:输出可预测。
paid == OrderStatus.PAID 处断点,把 paid 与 OrderStatus.PAID 同时加入 Watch,对比两者的 id 是否相同——这就是 == 能用的原因(单例);OrderStatus.fromLabel("已发货") 处按 F7 进入,观察 Arrays.stream(values()) 遍历的过程,看 filter 在哪个常量上匹配成功;switch 增删一个 case,观察编译器的反应(删掉 CANCELED 分支会直接编译失败);OrderStatus.values() 处看返回的数组长度,理解枚举常量在内存里就是几个单例对象。ordinal() 持久化 int code = status.ordinal(); // 存进数据库
// 后来有人在 CREATED 后面插入了一个新状态
// 所有历史数据的含义全变了修复:持久化用 name(),或给枚举加一个显式的业务编码字段(如 code)。
valueOf 传入不存在的名称 OrderStatus.valueOf("UNKNOWN"); // IllegalArgumentException: No enum constant修复:需要容错时自己写一个 fromLabel / fromName,返回 Optional 或抛带业务说明的异常(第 7 节的 fromLabel 就是模板)。
public enum Status {
A, B // 缺少分号
private final String x; // 编译错误
}规则:只要枚举里还有别的东西(字段、方法),常量列表后必须加分号。
equals 比较枚举与字符串 if (status.equals("PAID")) { } // 永远 false类型不同,直接不相等。正确写法是 status == OrderStatus.PAID,或用 status.name().equals("PAID")(但不推荐用字符串驱动业务逻辑)。
目标:把一段"用字符串表示状态"的代码改成枚举驱动。
步骤:
TicketStatus 枚举(待处理、处理中、已解决、已关闭),带中文名与"是否可编辑"两个属性;canTransitionTo 表示允许的流转:待处理 → 处理中/已关闭,处理中 → 已解决/待处理,已解决 → 已关闭,已关闭 → 无;EnumMap 统计一组工单的状态分布;switch 表达式加一个新状态,删除一个 case 分支,观察编译错误并解释原因。验收方式:能解释"为什么 switch 表达式对枚举会检查覆盖完整性"。
扩展任务:给 TicketStatus 加一个 fromLabel 方法(参考第 7 节),并处理"标签不存在"的情况——你选择抛异常还是返回 Optional?说明理由。
基础题
== 比较是安全的。(对 / 错)ordinal() 不适合持久化到数据库?代码阅读题
enum Level {
LOW(1), MID(5), HIGH(9);
private final int weight;
Level(int weight) {
this.weight = weight;
}
int weight() {
return weight;
}
}
System.out.println(Level.MID.weight());
System.out.println(Level.values()[2]);
System.out.println(Level.valueOf("LOW").ordinal());编程题
HttpMethod 枚举(GET、POST、PUT、DELETE),带两个属性:是否需要请求体、是否幂等。再写一个方法 String describe(HttpMethod method),用 switch 表达式返回描述。综合任务
ApprovalStatus:草稿、待审批、已通过、已驳回,每个状态带中文名;canTransitionTo 表示合法流转:草稿 → 待审批,待审批 → 已通过/已驳回,已驳回 → 草稿,已通过 → 无;isFinal() 方法判断是否为终态;EnumMap 统计一批单据的状态分布;main 演示:合法流转成功、非法流转被拒绝。提示 1(方向):第 4 题注意 values()[2] 是第三个常量。
提示 2(关键点):第 5 题的"是否幂等"可以按 HTTP 规范:GET、PUT、DELETE 幂等,POST 不幂等。
提示 3(接近答案):第 6 题的终态可以用 switch 表达式实现:已通过 与 已驳回 返回 true(驳回后还能回到草稿,所以它不是终态——请你判断)。
== 比较的是同一个引用,安全且比 equals 更快。private、分号(;)。ordinal() 是常量在声明列表中的位置。只要有人调整顺序或插入新常量,历史数据的含义就会整体错位。持久化应该用 name() 或自定义的业务编码。5 ← Level.MID.weight()
HIGH ← Level.values()[2](第三个常量,转字符串用 name())
0 ← Level.valueOf("LOW").ordinal()package cn.kb.learn.oop;
public enum HttpMethod {
GET(false, true),
POST(true, false),
PUT(true, true),
DELETE(false, true);
private final boolean hasBody;
private final boolean idempotent;
HttpMethod(boolean hasBody, boolean idempotent) {
this.hasBody = hasBody;
this.idempotent = idempotent;
}
public boolean hasBody() {
return hasBody;
}
public boolean idempotent() {
return idempotent;
}
public static String describe(HttpMethod method) {
return switch (method) {
case GET -> "查询资源,无请求体,幂等";
case POST -> "创建资源,有请求体,不幂等";
case PUT -> "整体更新资源,有请求体,幂等";
case DELETE -> "删除资源,无请求体,幂等";
};
}
public static void main(String[] args) {
for (HttpMethod method : values()) {
System.out.println(method + ": " + describe(method));
}
}
}package cn.kb.learn.oop;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public enum ApprovalStatus {
DRAFT("草稿"),
PENDING("待审批"),
APPROVED("已通过"),
REJECTED("已驳回");
private final String label;
ApprovalStatus(String label) {
this.label = label;
}
public String label() {
return label;
}
public boolean canTransitionTo(ApprovalStatus next) {
return switch (this) {
case DRAFT -> next == PENDING;
case PENDING -> next == APPROVED || next == REJECTED;
case REJECTED -> next == DRAFT;
case APPROVED -> false;
};
}
/** 终态:不允许再流转 */
public boolean isFinal() {
return this == APPROVED;
}
public static void main(String[] args) {
System.out.println("草稿 → 待审批: " + DRAFT.canTransitionTo(PENDING));
System.out.println("待审批 → 已通过: " + PENDING.canTransitionTo(APPROVED));
System.out.println("已通过 → 草稿: " + APPROVED.canTransitionTo(DRAFT) + "(终态不可变)");
System.out.println("已驳回 → 草稿: " + REJECTED.canTransitionTo(DRAFT) + "(可以打回重写)");
Map<ApprovalStatus, Integer> counter = new EnumMap<>(ApprovalStatus.class);
List<ApprovalStatus> records = List.of(DRAFT, PENDING, PENDING, APPROVED, REJECTED, DRAFT);
for (ApprovalStatus status : records) {
counter.merge(status, 1, Integer::sum);
}
System.out.println("状态分布: " + counter);
System.out.println("已通过是终态: " + APPROVED.isFinal());
System.out.println("已驳回是终态: " + REJECTED.isFinal() + "(还能回到草稿,所以不是终态)");
}
}关于"驳回是不是终态":业务上"驳回"意味着单据还能被修改后重新提交,所以它不是终态。这正是枚举要把业务判断写成代码的意义——"终态"的定义一旦有歧义,代码里的 isFinal() 就是唯一的事实来源。
预期输出:
草稿 → 待审批: true
待审批 → 已通过: true
已通过 → 草稿: false(终态不可变)
已驳回 → 草稿: true(可以打回重写)
状态分布: {DRAFT=2, PENDING=2, APPROVED=1, REJECTED=1}
已通过是终态: true
已驳回是终态: false(还能回到草稿,所以不是终态)switch 表达式处理枚举,并理解编译器的覆盖检查;== 为什么安全,以及 ordinal() 为什么不能持久化;EnumMap 做统计;int / String 常量。面向对象这条主线到这里完整了:类与对象 → 封装 → 继承 → 多态 → 抽象类与接口 → 枚举。
接下来按顺序推进 《String 与不可变性》 与 《equals、hashCode 与对象比较》,前者会解释"为什么字符串是不可变的",后者会讲清"怎么比较两个对象才正确"。
暂无