Java 17 源码级别包含密封类,并汇集了此前正式发布的记录类、instanceof 模式匹配、文本块和 switch 表达式。
「Java 17 可用」不等于「Java 17 首次引入」;类型模式 switch、记录模式和 when 守卫都不属于稳定的 Java 17 语法。
用当前 JDK 的 javac --release 17 编译全部源码和测试,并分别审查记录类的可变组件与密封层次的直接子类型。
是什么,为什么存在
Java 17 新特性不是一个彼此同时出现的语法包, 而是 Java 17 源码级别能够稳定使用的一组语言能力。 其中, 密封类(sealed class) 在 Java 17 正式发布;其余四项在 Java 14 到 Java 16 之间已经转正。 把它们放在同一迁移基线下讨论,有助于区分「目标项目可以使用什么」与「某项语法最初在哪个版本发布」。
这组能力解决的是常见的建模摩擦。
记录类(record class) 用声明状态的方式定义数据载体; 模式变量(pattern variable) 把类型检查与安全转换合并。
文本块(text block) 保留多行文本的源码结构; switch 表达式(switch expression) 让分支直接产生值。
密封类型则把「哪些直接子类型合法」变成编译器可以检查的约束。
这些特性并没有把 Java 变成另一种语言。
记录类仍然是类,文本块仍然产生 String,模式匹配仍然执行运行时类型检查,switch 表达式仍然遵循静态类型和完整性规则。
价值来自更准确地表达意图,而不是省下字符本身。
你会在升级到 Java 17 的代码库、库的最低 Java 版本声明、 构建插件配置以及 AI 生成的现代 Java 代码中遇到这些边界。 如果项目已经能使用 Java 17,这些构造通常可以逐步采用,不要求一次重写整个系统。 不过,源码级别、运行时版本和依赖项的类文件版本是三个不同的兼容性问题。
工作原理
五项特性对应五个正式版本
下表记录的是正式发布版本,不是预览版本。 Java 17 可以稳定编译表中全部语法,但只有密封类是在 Java 17 转正。
| 特性 | 正式版本 | 编译器提供的主要保证 |
|---|---|---|
switch 表达式 | Java 14 | 每条正常完成路径产生兼容的值 |
| 文本块 | Java 15 | 规范化换行、处理附带缩进和转义 |
instanceof 模式匹配 | Java 16 | 匹配成功路径中的类型绑定和流作用域 |
| 记录类 | Java 16 | 从组件声明派生成员与值语义方法 |
| 密封类 | Java 17 | 限制允许的直接子类或直接实现类 |
预览特性需要 --enable-preview,
并绑定到编译时所用的具体 JDK 版本。
生产代码若承诺稳定的 Java 17 源码级别,就不应靠预览开关获得类型模式 switch。
该语法后来继续变化,因此从新教程复制的 case Type value -> 或 when 守卫不能倒推为 Java 17 稳定语法。
记录类声明数据形状
record Account(String owner, Tier tier) 声明两个记录组件。
编译器据此提供同名私有 final 字段、规范构造器、owner() 与 tier() 访问器,以及基于全部组件的 equals、hashCode 和 toString。
记录类隐式继承 java.lang.Record,并且隐式为 final,所以不能再继承其他类或被普通类扩展。
紧凑规范构造器省略参数列表和显式字段赋值。 构造器体可以校验或重新绑定参数,随后编译器把最终参数值赋给字段。 这种写法适合在对象诞生时建立不变量,但它不会自动复制列表、数组或其他可变组件。
模式变量使用流作用域
表达式 value instanceof Account account 先检查运行时类型,成功后才绑定 account。
变量的可用范围由控制流决定,而不只是由最近的花括号决定。
因此,&& 右侧可以使用成功匹配后的变量;如果条件取反后立即返回,变量也可以在后续路径中使用。
null instanceof Account account 的结果是 false,不会绑定变量,也不会抛出异常。
模式匹配只消除重复的显式转换,不会验证对象内部字段,不会把开放类型层次变成封闭层次,也不会替代业务校验。
密封类型约束直接继承关系
sealed 类型通过 permits 列出允许的直接子类型;当允许的子类型与它声明在同一编译单元时,编译器也可以推断该列表。
每个直接子类型必须明确选择 final、sealed 或 non-sealed,记录类和枚举则可能隐式满足要求。
non-sealed 会重新开放该分支,所以「根类型密封」不等于整个后代集合永远有限。
在命名模块中,允许的直接子类型必须与密封类型属于同一模块。 在未命名模块中,它们必须属于同一包。 这些约束让类型所有者控制边界,也意味着跨模块插件接口通常不适合作为密封根类型。
文本块和 switch 仍有精确语义
文本块的开始定界符后
必须换行。
编译器会规范化行终止符、删除附带的公共缩进,再解释转义序列;结束定界符的位置会参与缩进计算。
结果只是普通 String,通常还包含结束定界符之前的最后一个换行。
switch 表达式必须覆盖选择器的所有可能值。
箭头规则不会贯穿;单个表达式直接提供结果,块规则则用 yield 提供结果。
在稳定的 Java 17 中,枚举适合展示这种完整性,而针对密封类型的模式 switch 仍属于预览边界。
示例
在一个 Java 17 程序中组合稳定特性
第一个示例把记录类、instanceof 模式变量、枚举 switch 表达式和文本块放在同一个源码文件中。
它没有使用预览开关,因而可以作为 Java 17 源码兼容性的最小烟雾测试。
public class ReleaseFeatures {
enum Tier { BASIC, PRO, ENTERPRISE }
record Account(String owner, Tier tier) {
Account {
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("owner is required");
}
owner = owner.strip();
}
}
static String label(Object value) {
if (!(value instanceof Account account)) {
return "unsupported";
}
int seats = switch (account.tier()) {
case BASIC -> 1;
case PRO -> 10;
case ENTERPRISE -> 100;
};
return """
%s (%s)
seats=%d
""".formatted(account.owner(), account.tier(), seats);
}
public static void main(String[] args) {
System.out.print(label(new Account(" Lin ", Tier.PRO)));
System.out.println(label(42));
}
}Lin (PRO)
seats=10
unsupported紧凑构造器先拒绝空名称,再把重新绑定后的 owner 写入记录字段。
否定的模式匹配分支返回后,后续代码可以把 account 当作已经绑定的 Account 使用。
枚举分支全部覆盖,所以不需要 default。
用密封接口建模封闭结果
Java 17 可以稳定声明密封层次,也可以对每个分支使用 instanceof 模式匹配。
这里故意不使用类型模式 switch;最后的异常分支同时防止未来修改造成静默遗漏。
import java.util.ArrayList;
import java.util.List;
public class SealedResult {
sealed interface Lookup permits Found, Missing {}
record Found(String id, List<String> roles) implements Lookup {
Found {
roles = List.copyOf(roles);
}
}
record Missing(String id) implements Lookup {}
static String describe(Lookup result) {
if (result instanceof Found found) {
return found.id() + ": " + found.roles().size() + " roles";
}
if (result instanceof Missing missing) {
return missing.id() + ": missing";
}
throw new IllegalStateException("unhandled result: " + result);
}
public static void main(String[] args) {
var sourceRoles = new ArrayList<>(List.of("reader", "author"));
var found = new Found("u-17", sourceRoles);
sourceRoles.add("admin");
System.out.println(describe(found));
System.out.println(describe(new Missing("u-18")));
System.out.println("source roles: " + sourceRoles.size());
System.out.println("snapshot roles: " + found.roles().size());
}
}u-17: 2 roles
u-18: missing
source roles: 3
snapshot roles: 2两个记录类隐式为 final,因此合法地关闭了密封接口的两个分支。
List.copyOf 让 Found 保存不可修改的快照;之后修改源列表不会改变记录的值。
如果组件本身是可变领域对象,还需要该对象自己的复制或不可变策略。
看清文本块的缩进和 yield
第三个示例让 switch 的块规则通过 yield 产生值,再把结果插入文本块。
结束定界符与内容的公共缩进被移除,内容内部额外的两个空格则会保留。
public class TextBlockReport {
enum Status { QUEUED, RUNNING, DONE }
static String render(Status status, int count) {
String detail = switch (status) {
case QUEUED -> "waiting";
case RUNNING -> {
if (count == 0) {
yield "starting";
}
yield "processed " + count;
}
case DONE -> "complete";
};
return """
status=%s
detail=%s
""".formatted(status, detail);
}
public static void main(String[] args) {
System.out.print(render(Status.RUNNING, 3));
System.out.print(render(Status.DONE, 0));
}
}status=RUNNING
detail=processed 3
status=DONE
detail=completeyield 只结束当前 switch 表达式的块规则,并把值交给表达式;return 则会试图结束整个方法。
文本块最后的换行来自结束定界符位于下一行。
如果结束定界符紧跟最后的内容,结果就不会自动带上该换行。
陷阱
修复: 分别记录正式版本:switch 表达式是 14,文本块是 15,记录类与 instanceof 模式匹配是 16,密封类是 17。
迁移文档可以把它们称为 Java 17 基线可用特性,但不要改写发布历史。
修复: 对承诺 Java 17 的构建使用 javac --release 17,且不要添加 --enable-preview。
在这个源码级别使用 instanceof 链、访问者或多态方法;需要正式的模式 switch 时,把最低版本明确提高到 Java 21。
修复: 在规范构造器中校验组件,并按所有权要求使用 List.copyOf、数组副本或不可变领域类型。
还要检查访问器是否会重新暴露可变存储;记录类提供的是浅层状态约束,不是递归冻结。
修复: 对真正由当前模块拥有的封闭枚举,列出全部常量并让编译器在源码重编译时暴露遗漏。 来自文本或网络的未知值应在解析边界单独处理;源码完整性也不能替代针对旧调用方的二进制兼容性测试。
修复: 使用对应格式的序列化器、转义器或参数化查询。
formatted() 只执行字符串格式化;把不可信输入填进 SQL 文本块,仍然会产生注入漏洞。
修复: 只在一个模块或包真正拥有类型集合时使用密封层次,并逐个审查直接子类型的修饰符。
扩展点应采用普通接口;如果某个分支有意开放,就不要把根类型的 permits 列表当作全部运行时类型清单。
迁移边界与编译结果
--release 同时约束语法和平台 API
在较新的 JDK 上,javac --release 17 会按 Java 17 的语言规则编译,并限制代码使用 Java 17 对应的已记录 Java SE API。
它还生成 Java 17 可识别的类文件;Java 17 的类文件主版本号是 61。
只设置当前 JDK 的默认编译选项,无法证明产物能在 Java 17 运行。
--release 不会检查第三方依赖是否也提供 Java 17 兼容的类文件,也不会证明反射、服务加载或框架代理在目标运行时行为一致。
因此,迁移验证至少要包含源码编译、依赖解析和真实 Java 17 运行测试。
本主题的示例还在 Java 25 上运行,以确认这些 Java 17 类文件可被当前目标 JDK 执行。
记录类在类文件中保留结构
记录类的类文件继承 java.lang.Record,并带有描述记录组件的 Record 属性。
字段、访问器和对象方法不是预处理器展开的源码文本,而是编译器依据记录声明生成的类成员和引导信息。
反射和序列化框架可以识别记录结构,但框架是否支持记录构造方式仍取决于该框架版本。
自动生成的相等性会逐组件比较。 这不会把数组变成按元素比较的值类型,也不会为可变组件建立快照。 如果记录要充当映射键或跨线程值,组件本身的相等性、稳定性和线程安全仍然属于设计契约。
密封关系是类文件契约
密封类或接口的类文件记录允许的直接子类型。 JVM 在加载相关类型时会执行约束,因此它不只是源码检查器使用的提示。 加入、移除或重新开放一个允许分支都可能影响外部编译代码,应作为 API 演化来审查。
Java 17 的稳定语法还不能用类型模式 switch 消费这个层次,所以示例使用 instanceof 链和显式兜底。
在 Java 21 或更高源码级别改成完整的模式 switch 后,新增允许分支通常会在重新编译消费者时暴露遗漏。
已经发布的旧类文件仍需要二进制兼容性测试,不能只依赖新一次编译。
模式变量主要改变源码控制流
instanceof 模式把检查、转换和局部变量绑定写成一个构造。
编译器仍会产生相应的运行时类型检查与转换操作,并用控制流证明变量在哪些路径已经绑定。
它不提供新的动态分派协议,也不会绕过类型擦除。
因此,value instanceof List<String> strings 仍然非法,因为运行时无法检查被擦除的元素类型实参。
可以匹配 List<?>,再按需求验证元素。
把模式变量引入更短,并不意味着后续转换、空值或集合内容已经安全。
文本块和 switch 不承诺性能捷径
文本块在编译期变成普通字符串常量或普通字符串构造的一部分。
switch 表达式也会变成与选择器类型相适应的控制流;语言规范承诺结果与完整性语义,不承诺某条源码形式必然更快。
没有基准和目标 JVM 配置时,不应把这些特性包装成性能优化。
迁移时应按表达能力选择它们:文本块用于看清多行内容,switch 表达式用于让有限分支产生一个值。
性能敏感路径仍需在目标运行时、真实数据分布和实际编译参数下测量。
源码变短本身不是性能证据。
4个问题 · 2 道输出预测题 · 1 道找错题