《Java 编程入门》5.2 switch 表达式与 yield

从 5.1 留下的 if/else 命令链出发,拆开 switch 的冒号写法、箭头写法与表达式写法,讲清 fall-through、yield 的作用域和枚举穷尽性检查;再用 library 的 Command 枚举与 dispatch 方法,把命令分发写成一张编译器能检查的表。

本节目标:能区分 switch 语句与 switch 表达式,知道什么时候必须写 yield,并会用枚举 + 穷尽性检查把命令分发变成可维护的表。
适用版本:JDK 25 LTS(Temurin 25.0.5);箭头 switch、switch 表达式、yield 在 JDK 21 上同样可用。

上一节留下的问题

5.1 的 dispatch 是一条 if/else if 链:

if (command.equals("add")) {
    handleAdd(parts);
} else if (command.equals("list")) {
    handleList();
} else if (command.equals("search")) {
    handleSearch(parts);
} else if (command.equals("count")) {
    System.out.println("books = " + books.size());
} else {
    System.out.println("unknown command: " + command);
}

它能跑,但有两个明显问题。

第一,命令名是散落的字符串。"add" 在 dispatch 里出现一次,在帮助文本里出现一次,将来在测试里还要再出现。拼错一个字母,编译器不会报错,只会在运行时走进 else。

第二,新增命令要改一条长链。命令从 5 条涨到 15 条时,这条链会横跨两屏,读者很难一眼确认「quit 到底有没有被处理」。

switch 解决的正是「按一个值选一条路」这类问题。但 Java 的 switch 有两代写法,行为差别不小,先把它们分开。

冒号写法:能穿透,也能漏 break

最老的 switch 是语句,用冒号分隔标签:

static String shelfOf(int year) {
    String shelf = "general";
    switch (year / 10) {
        case 202:
            shelf = "new";
        case 201:
            shelf = "modern";
            break;
        default:
            shelf = "archive";
    }
    return shelf;
}

本机 JDK 25 调用 shelfOf(2025),输出:

modern

按代码的意图,2025 年应该落在「新书」架,结果却是「现代」架。原因是 case 202 执行完没有 break,控制流穿透(fall through)到 case 201,把 shelf 又改了一次。

fall-through 不是 bug,它是 C 语言留下的设计,偶尔有用:多个标签共享同一段逻辑时,可以写 case 1: case 2: doSomething();。但在业务代码里,它的收益远小于代价——漏写一个 break 不会有任何编译警告,只会让结果悄悄错掉。 从 Java 14 起,箭头写法把这条路堵上了。

箭头写法:一个标签只做一件事

箭头形式的 switch 用 -> 代替冒号,标签后面只接一个表达式、一个代码块或一次 throw:

switch (command) {
    case ADD -> handleAdd(parts);
    case LIST -> handleList();
    case SEARCH -> handleSearch(parts);
    case COUNT -> System.out.println("books = " + books.size());
    case QUIT -> System.out.println("bye");
    case UNKNOWN -> System.out.println("unknown command: " + parts[0]);
}

箭头形式有三条确定的规则:

  • 不穿透。 每个标签执行完自动结束,不需要 break,也就不会漏。
  • 一个标签可以匹配多个值。 写成 case ADD, LIST -> ...,共享同一段处理。
  • 代码块用花括号。 需要多条语句时写 case ADD -> { ... },块内正常用分号。

这段代码是 switch 语句:它执行动作,不产出值。语句形式不要求覆盖所有枚举值,少写一个 case 也能编译,运行时匹配不到就什么都不做。这正是命令分发想要的吗?未必——漏掉 QUIT 时,程序不会报错,只会忽略退出命令。

表达式写法:switch 产出值

把 switch 放在赋值号右边、return 后面或方法参数里,它就是表达式,整个结构产出一个值:

static int shelfNumber(int year) {
    return switch (year / 10) {
        case 202 -> 1;
        case 201 -> 2;
        default -> 9;
    };
}

这里每个箭头右边是一个表达式,它的值就是整个 switch 的值。没有 break,没有中间变量,return 直接返回结果。

表达式形式多了一条硬性要求:必须穷尽所有可能。 对 int 这类值,编译器无法枚举全部输入,所以必须有 default;对枚举,编译器知道所有常量,只要每个常量都覆盖,就可以不写 default——这一点下一节会用到。

如果某个分支需要多条语句,就不能直接写表达式,要用代码块加 yield:

static String describe(Command command) {
    return switch (command) {
        case ADD -> "add a book";
        case QUIT -> {
            System.out.println("shutting down");
            yield "quit";
        }
        default -> "other";
    };
}

yield 的意思是「这个代码块产出的值」。它只能出现在 switch 表达式的块里,不能出现在普通方法里,也不是 return——yield 结束的是当前分支,return 结束的是整个方法。

yield 的三条边界

边界一:只在 switch 表达式里

把 yield 写进普通方法,编译直接失败。本机 JDK 25 的报错是:

错误: yield 在 switch 表达式外部
        yield 1;
        ^

JDK 13 引入 switch 表达式时,yield 才成为受限标识符;JDK 14 随 switch 表达式一起转正。在此之前它不是关键字,有些老代码把它当变量名。受限标识符的意思是:大多数位置仍可作标识符,但在 switch 块里会被当成关键字。新代码不要再用它命名。

边界二:块里每条路径都要 yield

case ADD -> {
    if (parts.length > 1) {
        handleAdd(parts);
        yield true;
    }
    // 缺少 yield
}

这段编译失败,本机 JDK 25 的报错是:

错误: switch 规则完结但没有提供值
  (switch 表达式中的 switch 规则必须提供值或抛出)

编译器要求块的所有正常出口都 yield 一个同类型的值;抛异常的分支可以不 yield,因为它不会正常返回。

边界三:yield 与 break 不混用

箭头形式里不能用 break 跳出分支,也不能写 break value。需要提前结束时用 yield;如果只是想跳出循环,那属于外层循环的 break,和 switch 无关。在 switch 表达式的块里写 break;,本机 JDK 25 报「尝试从 switch 表达式中断」;写 break 1; 则直接报「需要’;’」。

把命令变成枚举

switch 最适合的搭档是枚举。把命令名收进一个类型,字符串只在一个地方出现:

package com.plume.library;

public enum Command {
    ADD, LIST, SEARCH, COUNT, QUIT, UNKNOWN;

    public static Command from(String token) {
        return switch (token) {
            case "add" -> ADD;
            case "list" -> LIST;
            case "search" -> SEARCH;
            case "count" -> COUNT;
            case "quit" -> QUIT;
            default -> UNKNOWN;
        };
    }
}

这段代码有三层收益。

字符串只在 from 里出现一次。 其他代码只认 Command.ADD,拼写错误在编译期就被拦住——Command.AD 不存在,编译器直接报「找不到符号」。

未知命令有明确的归宿。 UNKNOWN 是一个真实常量,不是「什么都不做」。它可以被记录、被统计、被打印帮助,而不是悄悄消失。

外层 switch 可以穷尽检查。 因为枚举的常量集合是封闭的,编译器能确认每个常量都被处理。少写一个,编译失败:

static String label(Command c) {
    return switch (c) {
        case ADD -> "add";
        case LIST -> "list";
    };
}

本机 JDK 25 的报错:

错误: switch 表达式不包含所有可能的输入值
        return switch (c) {
               ^

这条报错的价值在于:新增 Command.RETURN 时,所有没处理它的 switch 表达式会一起编译失败。 编译器替你列出了所有需要改的地方,比运行时发现「还书命令没反应」便宜得多。

from 里的 default -> UNKNOWN 是必要的,因为 switch 的选择器是 String,编译器不知道用户会输入什么。但这一层 default 把「未知输入」收敛成了一个值,外层就不需要再写 default 了。

在 library 里落地

Command 和 LibraryApp 的分发方法一起看。Book 仍沿用第 4 章的五个公开字段;这里只列分发相关的代码。

private boolean dispatch(Command command, String[] parts) {
    return switch (command) {
        case ADD -> {
            handleAdd(parts);
            yield true;
        }
        case LIST -> {
            handleList();
            yield true;
        }
        case SEARCH -> {
            handleSearch(parts);
            yield true;
        }
        case COUNT -> {
            System.out.println("books = " + books.size());
            yield true;
        }
        case QUIT -> {
            System.out.println("bye");
            yield false;
        }
        case UNKNOWN -> {
            System.out.println("unknown command: " + parts[0]);
            yield true;
        }
    };
}

返回 boolean 表示「是否继续」。QUIT 返回 false,主循环据此退出;其他命令返回 true。这样「退出」不再是循环体里一个特殊的 break,而是分发结果的一部分,测试时可以直接断言 dispatch(QUIT, ...) 返回 false,不需要真的跑一个循环。

主循环相应简化:

void run() throws IOException {
    String line;
    while ((line = in.readLine()) != null) {
        line = line.strip();
        if (line.isEmpty()) {
            continue;
        }
        String[] parts = line.split("\\s+", 2);
        if (!dispatch(Command.from(parts[0]), parts)) {
            break;
        }
    }
}

把 Command.from 和 dispatch 放进同一个工程,编译并回放:

javac -d out src/com/plume/library/*.java
java -cp out com.plume.library.LibraryApp

输入与输出(本机 JDK 25):

add 978-7-111-1|Effective Java|Joshua Bloch
added: Effective Java
add 978-7-115-2|Java Concurrency in Practice|Brian Goetz
added: Java Concurrency in Practice
list
1. Effective Java / Joshua Bloch
2. Java Concurrency in Practice / Brian Goetz
search Java
hit: Effective Java
hit: Java Concurrency in Practice
hits = 2
count
books = 2
remove x
unknown command: remove
quit
bye

注意 remove x 那两行:remove 不在命令表里,Command.from 把它收敛成 UNKNOWN,dispatch 打印一行提示后继续循环,程序没有崩,也没有静默忽略。

与 5.1 的版本相比,这次改动带来两个可检验的差别:

  • 新增命令时,编译器会点名。 在 Command 里加一个 RETURN,dispatch 立刻编译失败,直到你补上对应分支。
  • 命令名不再散落。 LibraryApp 里没有任何 "add" 字面量,只有 Command.ADD;字符串比较只发生在 from 这一个入口。

常见错误与边界

在冒号 switch 里漏 break。 前面 shelfOf(2025) 输出 modern 就是例子。箭头写法没有这个问题;如果维护的是老代码,看到冒号 case 就要逐个确认 break、return 或 throw 是否收尾。

重复的 case 标签。 两个标签匹配同一个值,编译失败:

错误: case 标签重复
            case 1 -> 20;
            ^

枚举里两个常量不会相等,所以这条错误通常出现在 int、String 或模式匹配的重叠类型上。编译器只查「常量值相同」,查不出「逻辑上重叠」,那要靠测试。

对 null 做 switch。 传统 switch 在选择器为 null 时抛 NullPointerException。本机 JDK 25 对 String 选择器:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "<local2>" is null
	at NullSwitch.main(NullSwitch.java:4)

从 JDK 21 起,可以用 case null -> 显式处理,或者 case null, default -> 把 null 和默认分支合并:

static String describe(String token) {
    return switch (token) {
        case null -> "null-token";
        case "add" -> "add";
        default -> "other";
    };
}

本机 JDK 25 依次传入 null、"add"、"list",输出 null-token、add、other。如果不写 case null,null 仍然会抛异常——case null 是显式选择,不是自动兜底。

在 switch 表达式里忘记 default。 对 String、int 这类开放类型,编译器要求穷尽,缺 default 会报「switch 表达式不包含所有可能的输入值」。对枚举则相反:覆盖了所有常量之后不要再写 default,否则新增常量时编译器不会提醒你,穷尽性检查就失效了。

把 yield 当成 return。 yield 只结束当前分支,方法继续往下走。yield 之后同一层的语句不可达,编译器会报「无法访问的语句」;如果 yield 在 if 里,if 之后的代码仍可能执行。需要提前结束整个方法时,用 return。

小结

  • 冒号 switch 会 fall-through,漏 break 不报错但结果会错;箭头 switch 不穿透,一个标签只做一件事。
  • switch 语句执行动作,switch 表达式产出值;表达式必须穷尽所有输入。
  • yield 只出现在 switch 表达式的块里,负责产出一个值;它不等于 return,也不能和 break value 混用。
  • 把命令名收进枚举,字符串只在一个 from 方法里出现,拼写错误变成编译错误。
  • 对枚举的 switch 表达式覆盖全部常量后不要再写 default,这样新增常量时编译器会点名所有待改位置。
  • library 的 dispatch 现在返回「是否继续」,退出逻辑从循环体移进了分发结果,更容易测试。

命令分发已经清楚了,但 handleAdd 里的参数校验还在用「先转成字符串再解析」的笨办法。5.3 会换一个角度:当输入的类型不确定时,如何用 instanceof 模式匹配安全地判断并取值。

阅读导航:上一节:5.1 if、for、while:把流程写成可读的句子 · 下一节:5.3 instanceof 模式匹配与守卫 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Netty 高性能网络编程》15.3 linkd-v2 验收与复盘
  2. 《Netty 高性能网络编程》15.2 限流、超时与降级
  3. 《Netty 高性能网络编程》15.1 网关路由与转发