JDK 28 的 javac 开始检查 switch 的 when 守卫。守卫里抛 IOException,在 27.0.1.0.3 上能编过,套 try/catch (IOException) 反而报 never thrown,跑起来照样炸。换到 28-ea+21-1707,同一份源码直接要求 catch 或 throws。
根因是 JDK-8390759。旧 Flow 分析扫 case 标签和分支体,漏了 guard。受检异常在编译期被“藏起来”,运行期还是会抛。JDK 28 把守卫重新纳入异常与可达性分析。
谁会撞上
业务里用模式匹配 switch,在 when 里调文件、网络、JDBC 这类声明了受检异常的方法。Spring Boot 控制器把 payload 分类,守卫里读 marker 文件大小,就是典型路径。
更绕的是:你在 27 上已经意识到可能抛 IOException,想包一层 catch,javac 却说 try 里根本不会抛。于是只好吞 Exception,或者干脆不处理。CI 升到 28 后,未处理的那条路径开始编译失败。
现场长什么样
最小探针(与 JBS / 官方测试同款):
import java.io.IOException;
public class GuardUnreported {
private static boolean guard() throws IOException {
throw new IOException("boom");
}
public static void main(String[] args) {
Object o = "x";
switch (o) {
case String s when guard() -> System.out.println("hit");
default -> System.out.println("other");
}
}
}
实测(两边都加 --release 21):
===== javac javac 27.0.1.0.3 :: probes/GuardUnreported.java =====
java openjdk version "27.0.1.0.3" 2026-10-25
exit=0
Exception in thread "main" java.io.IOException: boom
at GuardUnreported.guard(GuardUnreported.java:5)
at GuardUnreported.main(GuardUnreported.java:11)
run_exit=1
===== javac javac 28-ea :: probes/GuardUnreported.java =====
java openjdk version "28-ea" 2027-03-23
probes/GuardUnreported.java:11: error: unreported exception IOException; must be caught or declared to be thrown
case String s when guard() -> System.out.println("hit");
^
1 error
exit=1
再看 catch。27 上写了 catch,编译器不认;28 上同一段能编过,运行还能接到消息:
===== javac javac 27.0.1.0.3 :: probes/GuardCatch.java =====
probes/GuardCatch.java:15: error: exception IOException is never thrown in body of corresponding try statement
} catch (IOException e) {
^
1 error
exit=1
===== javac javac 28-ea :: probes/GuardCatch.java =====
exit=0
caught: boom
run_exit=0
方法上声明 throws IOException,两边都能编过,运行时行为一致,都会抛 boom。
发行说明里另一半是可达性。守卫里嵌套 switch expression,yield 后面再写语句,27 放过,28 报 unreachable:
===== javac javac 27.0.1.0.3 :: probes/GuardUnreachable.java =====
exit=0
===== javac javac 28-ea :: probes/GuardUnreachable.java =====
probes/GuardUnreachable.java:6: error: unreachable statement
case 0 -> { yield false; System.out.println(); }
^
1 error
exit=1
怎么确认
固定两套 javac:27.0.1.0.3+1-2 与 28-ea+21-1707,都加 --release 21。上面矩阵已经说明:语言级别没变,变的是编译器对守卫的分析。
落到 Spring Boot 4.1.2。服务里用模式匹配分类请求,守卫调用声明了 IOException 的私有方法:
public String classifyHidden(Object payload, Path marker) {
return switch (payload) {
case String s when existsNonEmpty(marker) -> "string-with-marker";
case String s -> "string-plain";
case Integer n when n > 0 -> "positive-int";
default -> "other";
};
}
private static boolean existsNonEmpty(Path marker) throws IOException {
return Files.isRegularFile(marker) && Files.size(marker) > 0;
}
JDK 27.0.1.0.3 上 mvn -DskipTests clean package exit=0。接口实测:
GET /classify?kind=hello
{"result":"string-plain"}
GET /classify?kind=hello&marker=/tmp/sg-marker.txt
{"result":"string-with-marker"}
GET /classify?kind=int
{"result":"positive-int"}
同一份源码换 28-ea+21,maven-compiler-plugin 3.15.0,release 仍是 21:
[INFO] Compiling 3 source files with javac [debug parameters release 21] to target/classes
[ERROR] COMPILATION ERROR :
[ERROR] PayloadGate.java:[27,46] unreported exception java.io.IOException; must be caught or declared to be thrown
[INFO] BUILD FAILURE
连带断点
- 27 上“正确写法”catch IOException,会先被 javac 挡下。别把 never thrown 理解成“运行也不会抛”。
- 吞
Exception/Throwable能骗过 27,但会把守卫里的受检异常和其他运行时问题搅在一起。28 升上来之后,更干净的做法是声明 throws,或在守卫外先算好布尔条件。 - 本机 IDE 仍绑 27 的 javac、CI 已是 28,会表现为本地绿、流水线编译红。对齐工具链版本再比。
- 和 raw/参数化三元那条(JDK-8385925)不是一回事。那条是结果类型判定;这条是守卫的异常与可达性漏扫。
升级怎么选
升 JDK 28 工具链前,先搜 when 后面有没有调声明了受检异常的方法。能编过的旧代码,优先改成:守卫只做纯判断,IO 放在 switch 外;或者给方法补 throws / 局部 try-catch。
已经写了 catch IOException 却在 27 上编不过的,换 28 后反而能过,这是预期修复,不是回归。
参考:JDK-8390759,JDK 28 Early-Access Release Notes(2026-11-02 更新稿含本条)。
一起交流
分享你的思考,让讨论更进一步。