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道找错题