把工具链换到 JDK 28 之后,一段以前能编过的三元表达式突然挂了:(b ? typed : raw).get(0) 返回 Integer 时,javac 报 Object cannot be converted to Integer。同一行在 JDK 27.0.1.0.3 上 exit=0,在 28-ea+21-1707 上 exit=1。开关不在 --release,在编译器版本。

根因是 JDK-8385925。standalone 三元 / switch 混用 raw 和参数化类型时,旧 javac 用赋值兼容(含 unchecked)挑结果类型,操作数顺序能决定能不能过;JDK 28 改回按子类型关系,两端都落到 raw List,get(0) 就是 Object。

谁会撞上

Spring Boot 服务里还留着 raw List 的老缓存、老 JDBC/反射工具,旁边已经是 List<Integer> / List<String>。业务代码用三元或 switch 在两条路径间挑一个列表,再直接 .get(0) 赋给具体类型。

更隐蔽的是「只换 CI 的 JDK」。pom 仍写 release 21,本机 JDK 27 一切绿灯;流水线升到 28,编译阶段直接红。错误信息看不出「顺序曾经救过你」,只会说 Object 转不了 Integer。

raw 在前的写法两边本来就会挂。只有「参数化在前、raw 在后」那一支,是 27 能过、28 不过。

现场长什么样

最小探针(官方发行说明同款):

import java.util.List;

class TernaryParamFirst {
    Integer first(boolean b, List raw, List<Integer> parameterized) {
        return (b ? parameterized : raw).get(0);
    }
}

实测:

===== TernaryParamFirst =====
--- JDK 27.0.1.0.3+1-2 ---
javac 27.0.1.0.3
exit=0

--- JDK 28-ea+21-1707 ---
javac 28-ea
probes/TernaryParamFirst.java:4: error: incompatible types: Object cannot be converted to Integer
        return (b ? parameterized : raw).get(0);
                                            ^
1 error
exit=1

把操作数对调成 raw 在前,两边都挂:

===== TernaryRawFirst (both fail) =====
--- JDK 27.0.1.0.3+1-2 ---
probes/TernaryRawFirst.java:4: error: incompatible types: Object cannot be converted to Integer
        return (b ? raw : parameterized).get(0);
                                            ^
1 error
exit=1
--- JDK 28-ea+21-1707 ---
probes/TernaryRawFirst.java:4: error: incompatible types: Object cannot be converted to Integer
        return (b ? raw : parameterized).get(0);
                                            ^
1 error
exit=1

standalone switch 同一套规则。参数化分支写在前面时,27 过、28 挂:

===== SwitchParamFirst =====
--- JDK 27.0.1.0.3+1-2 ---
exit=0
--- JDK 28-ea+21-1707 ---
probes/SwitchParamFirst.java:7: error: incompatible types: Object cannot be converted to Integer
        }).get(0);
              ^
1 error
exit=1

怎么确认

先固定两套 javac,都加 --release 21,排除「语言级别变了」的猜测。上面对照已经说明:release 不变,换编译器版本就分叉。

再落到 Spring Boot 4.1.2 工程。服务里留一段「参数化在前」的挑选逻辑:

@SuppressWarnings("rawtypes")
public Integer pickParamFirst(boolean useTyped, List raw, List<Integer> typed) {
    return (useTyped ? typed : raw).get(0);
}

JDK 27.0.1.0.3 上 mvn -DskipTests clean compile exit=0。同一份源码换 28-ea+21:

[INFO] Compiling 3 source files with javac [debug parameters release 21] to target/classes
[ERROR] COMPILATION ERROR :
[ERROR] LegacyListPicker.java:[12,44] incompatible types: java.lang.Object cannot be converted to java.lang.Integer
[ERROR] LegacyListPicker.java:[20,15] incompatible types: java.lang.Object cannot be converted to java.lang.Integer
[INFO] BUILD FAILURE

两行分别对应三元和 switch。maven-compiler-plugin 3.15.0,release 仍是 21。

连带断点

  • 只改操作数顺序不是修复。28 上两种顺序都按 raw List 定型,对调只会从「碰巧能过」变成「两边都不能过」的另一种写法。
  • @SuppressWarnings("rawtypes") / unchecked 压不住这条。这是结果表达式类型,不是警告。
  • IDE 若仍绑着 JDK 27 的 javac,本地绿、CI 红,会非常像「流水线脏了」。对齐工具链版本再比。
  • 和常量三元 null 在前调 length 崩池那条不是一回事。那条是常量折叠/常量池断言;这条是 raw 与参数化混用时的类型判定。

升级怎么选

优先消 raw。接口、缓存、工具方法改成 List<?> 或具体参数化类型,三元两边类型一致,问题直接消失。

短期过编译:先接到 List<?> / Object,再显式转换。

Integer first(boolean b, List raw, List<Integer> parameterized) {
    Object o = (b ? parameterized : raw).get(0);
    return (Integer) o;
}

这版在 27.0.1.0.3 和 28-ea+21 上 javac 都是 exit=0。跑通后的安全路径用 Spring Boot 4.1.2 + 28-ea+21 起服务:

{"java.version":"28-ea","useTyped":true,"value":1,"spring.boot.version":"4.1.2"}
{"java.version":"28-ea","useTyped":false,"value":7,"spring.boot.version":"4.1.2"}

升级清单可以加一条:全库搜 raw List/Map 出现在三元或 switch 结果里、又立刻按具体类型使用的调用点。先在 JDK 28 EA 上编一轮,比等到 GA 再堵 CI 便宜。

参考:OpenJDK 发行说明 javac Correctly Types Conditional and Switch Expressions Mixing Raw and Parameterized Types (JDK-8385925),以及 JBS JDK-8385925(Resolved in Build b17)。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。