枚举与常用标准库
学习目标
- 使用
enum表达固定选项集合。 - 为枚举添加字段、构造器和业务方法。
- 使用
Optional<T>表达可能不存在的查询结果。 - 使用
Objects.requireNonNull做必填参数校验。 - 使用
StringJoiner生成稳定摘要文本。 - 使用
Comparator描述排序规则。 - 使用
EnumMap按枚举键统计数据。
前置知识
已完成Lambda 与 Stream API,能够阅读 stream、filter、sorted、Optional、方法引用和 JUnit 断言。
为什么需要枚举
学习任务有固定状态:
- 待开始。
- 进行中。
- 已完成。
- 受阻。
如果用字符串表达状态,代码很容易写错:
String status = "todo";
String anotherStatus = "TODO";
String typo = "todoo";这些值都能编译通过,但业务含义并不一致。枚举把固定选项写进类型系统:
StudyTaskStatus status = StudyTaskStatus.TODO;这样调用者只能从定义好的常量中选择,拼写错误会在编译期暴露。
定义枚举
枚举使用 enum 定义:
public enum StudyTaskStatus {
TODO,
IN_PROGRESS,
DONE,
BLOCKED
}枚举常量通常使用全大写命名。它们不是普通字符串,而是 StudyTaskStatus 类型的固定对象。
枚举字段和方法
枚举也可以有字段、构造器和方法:
public enum StudyTaskStatus {
TODO("待开始"),
IN_PROGRESS("进行中"),
DONE("已完成"),
BLOCKED("受阻");
private final String displayName;
StudyTaskStatus(String displayName) {
this.displayName = displayName;
}
public String displayName() {
return displayName;
}
public boolean open() {
return this != DONE;
}
}这里 displayName 用于展示中文名称,open 用于判断任务是否仍需要处理。把行为放进枚举,比在各处写 status != DONE 更集中。
优先级也可以建模成枚举:
public enum StudyTaskPriority {
LOW(1, "低"),
MEDIUM(2, "中"),
HIGH(3, "高");
}weight 可以用于排序,displayName 可以用于摘要文本。
Objects:必填参数校验
Objects.requireNonNull 是常见空值校验工具:
this.status = Objects.requireNonNull(status, "status must not be null");
this.priority = Objects.requireNonNull(priority, "priority must not be null");它会在参数为 null 时抛出 NullPointerException,并带上你提供的消息。前面课程常用 IllegalArgumentException 表达业务输入不合法;这里用 Objects.requireNonNull 表达“这个引用必须存在”。
不要把它当成所有校验的替代品。标题为空白仍然需要业务校验:
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title must not be blank");
}Optional:可能没有结果
查询任务时,可能找得到,也可能找不到。返回 null 会把风险推给调用者。Optional<T> 可以明确表达“可能没有”:
public Optional<StudyTask> findByTitle(String title) {
String normalizedTitle = normalizeTitle(title);
return tasks.stream()
.filter(task -> task.title().equalsIgnoreCase(normalizedTitle))
.findFirst();
}调用者可以判断:
Optional<StudyTask> task = board.findByTitle("Optional");
task.ifPresent(found -> System.out.println(found.summary()));本章只使用 Optional 表达查询结果,不把它作为字段保存。StudyTask 内部可以保存一个可空 dueDate,对外再返回 Optional<LocalDate>:
public Optional<LocalDate> dueDate() {
return Optional.ofNullable(dueDate);
}StringJoiner:稳定拼接文本
当摘要由多个部分组成时,StringJoiner 比手动处理分隔符更清楚:
StringJoiner joiner = new StringJoiner(" | ");
joiner.add(priority.displayName())
.add(status.displayName())
.add(title);
dueDate().ifPresent(date -> joiner.add("due " + date));
return joiner.toString();输出示例:
高 | 待开始 | Stream 复盘 | due 2026-07-05如果任务没有截止日期,最后一段不会出现,也不会留下多余分隔符。
Comparator:描述排序规则
任务看板需要把开放任务排在一起,先看优先级,再看截止日期,最后看标题:
private static Comparator<StudyTask> taskOrder() {
return Comparator
.comparing(StudyTask::priority,
Comparator.comparingInt(StudyTaskPriority::weight).reversed())
.thenComparing(task -> task.dueDate().orElse(LocalDate.MAX))
.thenComparing(StudyTask::title);
}这段规则可以读成:
- 按优先级权重降序。
- 优先级相同时,截止日期更早的排前面。
- 仍然相同时,按标题排序。
LocalDate.MAX 表示“没有截止日期时放到较晚位置”。这比写很多嵌套 if 更容易维护。
EnumMap:按枚举统计
如果键是枚举,EnumMap 是专门为枚举键设计的 Map:
Map<StudyTaskStatus, Integer> counts = new EnumMap<>(StudyTaskStatus.class);
for (StudyTaskStatus status : StudyTaskStatus.values()) {
counts.put(status, 0);
}
for (StudyTask task : tasks) {
counts.merge(task.status(), 1, Integer::sum);
}先为每个状态放入 0,可以保证即使某个状态没有任务,摘要里也能显示出来:
待开始=1, 进行中=1, 已完成=1, 受阻=0可运行代码
StudyTaskStatus展示带字段和方法的枚举状态。StudyTaskPriority展示带权重的枚举优先级。StudyTask展示Objects、Optional和StringJoiner。StudyTaskBoard展示Comparator、EnumMap、查询和排序。StudyTaskBoardTest覆盖枚举行为、摘要、排序、查询、统计、只读返回和非法输入。- 面向对象练习 提供同主题 starter/solution。
使用 Java 21 运行对应测试:
mvn -B -pl examples/java-core -am test
mvn -B -pl exercises/object-oriented/starter -am compile
mvn -B -pl exercises/object-oriented/solution -am test常见错误与调试提示
- 用字符串保存固定状态:拼写错误很难提前发现。
- 在多个地方重复判断枚举状态:可以把简单行为放进枚举方法。
- 滥用
Optional.get():先判断或使用ifPresent、orElse等方法。 - 把
Optional作为所有字段类型:初学阶段优先把它用于返回值。 - 忘记
Objects.requireNonNull抛出的是NullPointerException。 - 手动拼接分隔符:多字段摘要优先考虑
StringJoiner。 - 排序规则散落在多个地方:把
Comparator抽成一个清楚的方法。
调试这类代码时,先看枚举常量是否符合业务选项,再看 Optional 是否为空,最后看排序规则中每一段比较条件是否符合预期。
最佳实践及适用边界
- 固定选项集合优先考虑枚举,而不是字符串常量。
- 枚举可以保存展示名、权重和简单业务判断。
- 查询可能没有结果时,返回
Optional<T>比返回null更清楚。 - 必填引用可以用
Objects.requireNonNull快速校验。 - 复杂排序用
Comparator组合规则,避免巨大条件分支。 - 枚举键统计可以考虑
EnumMap。
分级练习
- 基础:实现
StudyTaskStatus.open、StudyTaskPriority.weight和任务摘要。 - 进阶:实现
StudyTaskBoard.findByTitle和firstOverdueTask,用Optional表达可能没有结果。 - 挑战:实现
openTasksByPriority和statusSummary,使用Comparator、EnumMap和StringJoiner。
面试与复习题
- 枚举相比字符串状态有什么优势?
- 枚举为什么也可以有字段和方法?
Objects.requireNonNull适合解决什么问题?Optional<T>表达什么语义?- 为什么不建议随手调用
Optional.get()? StringJoiner相比手动拼接分隔符有什么好处?Comparator.thenComparing适合什么场景?- 什么情况下可以考虑使用
EnumMap?
本章总结
枚举把固定业务选项变成类型,能减少字符串拼写错误,并把简单行为集中到枚举内部。配合 Optional、Objects、StringJoiner、Comparator 和 EnumMap,日常 Java 代码可以更明确地表达空值、文本拼接、排序和统计规则。
下一步
下一章:Java 核心综合复盘
继续查看:学习路线