如果你还在用 JDK 8 或 11,是时候重新审视一下你的开发工具链了。JDK 17 作为最新的长期支持版本,它带来的远不止是几个语法糖或性能数字的提升。很多开发者对它的认知还停留在“又一个新版本”,但实际上,从项目构建、代码安全到运行时效率,JDK 17 引入的一系列特性正在悄然改变 Java 开发的默认范式。
这篇文章不会简单罗列官方文档里的特性清单。我们将聚焦于那些真正能影响你日常编码、提升项目质量的“硬核”特性。你会看到,有些特性能帮你写出更简洁、更安全的代码;有些特性则从底层优化了 JVM,让你的应用跑得更快、更稳;还有一些特性,虽然看似微小,却可能成为你解决特定棘手问题的“银弹”。
无论你是正在评估新项目的基础技术栈,还是希望为现有系统进行安全、平稳的升级,理解 JDK 17 的核心价值都至关重要。接下来,我们将从实际开发场景出发,深入解读这些特性,并提供可运行的代码示例和升级避坑指南。
1. 这篇文章真正要解决的问题
对于大多数 Java 开发者而言,面对 JDK 新版本时最大的困惑往往是:除了版本号更新,它到底能为我解决什么实际问题?是能让我的代码少写几行,还是能让线上服务更稳定?JDK 17 作为继 JDK 8 和 11 之后的又一个长期支持版本,其意义绝不仅仅是“又一个选择”。
它核心解决的是三个层面的问题:
- 开发效率与代码质量:通过引入更现代的语法和 API,减少模板代码,让意图更清晰,从语言层面降低出错概率。
- 应用性能与稳定性:底层 JVM 的持续优化,如新的垃圾回收器、即时编译器的改进,直接带来吞吐量提升和延迟降低。
- 安全与可维护性:强封装、弃用警告移除等举措,促使开发者远离不安全的旧实践,构建更健壮、未来兼容性更好的系统。
本文将重点剖析那些具有高实用价值和迁移影响的关键特性,帮助你判断 JDK 17 是否适合你的项目,以及如何平滑、安全地完成升级。
2. 核心特性概览与适用场景
在深入细节之前,我们先对 JDK 17 的核心特性做一个全景式扫描。根据其影响范围和实用性,我们可以将其分为以下几类:
| 特性类别 | 代表特性 | 主要价值 | 适用场景 |
|---|---|---|---|
| 语言语法增强 | 密封类、模式匹配、文本块 | 增强代码表现力,强化领域建模,减少冗余代码。 | 所有新项目;老项目重构关键模块时。 |
| API 更新与新增 | 新的随机数生成器、增强的伪随机数生成器 | 提供更安全、性能更好的标准库实现。 | 涉及加密、安全、模拟、测试的模块。 |
| JVM 性能优化 | 新的即时编译器优化、ZGC/Shenandoah 改进 | 提升应用吞吐量,降低延迟,优化内存使用。 | 高并发、低延迟、大内存服务端应用。 |
| 安全与强封装 | 强封装 JDK 内部 API、移除 RMI 激活机制 | 提升运行时安全,促使代码依赖更规范的 API。 | 所有项目,尤其是对安全有要求或需要长期维护的项目。 |
| 预览/孵化器特性 | 外部函数与内存 API、向量 API | 为未来访问本地代码和利用 SIMD 指令集铺路。 | 高性能计算、机器学习、图形处理等前沿领域。 |
对于大多数业务开发团队,语言语法增强和安全与强封装是升级最直接的理由。而JVM性能优化则是服务端应用获得“免费午餐”的关键。接下来,我们将挑选其中最值得关注的特性进行详解。
3. 环境准备与前置条件
在开始体验任何特性之前,你需要一个可运行的 JDK 17 环境。
1. 下载与安装访问 Oracle 官网或 Adoptium 等开源发行版网站,下载适用于你操作系统的 JDK 17 安装包。对于生产环境,建议选择提供长期支持的发行版,如 Eclipse Temurin。
2. 验证安装打开终端或命令提示符,执行以下命令验证安装是否成功:
java -version预期输出应类似于:
openjdk version "17.0.10" 2024-01-16 OpenJDK Runtime Environment Temurin-17.0.10+7 (build 17.0.10+7) OpenJDK 64-Bit Server VM Temurin-17.0.10+7 (build 17.0.10+7, mixed mode, sharing)3. IDE 配置确保你的 IDE(如 IntelliJ IDEA 或 Eclipse)已配置使用 JDK 17 作为项目的 SDK 和语言级别。在 IntelliJ IDEA 中,可以通过File -> Project Structure -> Project进行设置。
4. 构建工具配置在 Maven 的pom.xml中,配置编译插件以使用 JDK 17:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <!-- 如需使用预览特性,需添加以下参数 --> <!-- <compilerArgs>--enable-preview</compilerArgs> --> </configuration> </plugin> </plugins> </build>在 Gradle 的build.gradle中配置:
plugins { id 'java' } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } tasks.withType(JavaCompile) { options.compilerArgs += '--enable-preview' // 仅在需要预览特性时启用 }环境就绪后,让我们进入第一个重量级特性:密封类。
4. 密封类:塑造更严谨的领域模型
密封类是 JDK 17 中正式转正的特性(在 JDK 15 和 16 中为预览特性)。它解决了面向对象设计中一个经典问题:如何精确控制一个类或接口的继承/实现层次。
没有密封类时的问题:假设你定义了一个表示“形状”的接口Shape,你只希望有Circle和Rectangle两种具体实现。但在传统的 Java 中,任何其他类都可以实现Shape接口,这破坏了你的领域模型约束,也使得使用instanceof进行穷尽判断时,编译器无法提供帮助。
密封类的解决方案:使用sealed关键字修饰类或接口,并用permits子句明确指定哪些类可以继承或实现它。
// 文件路径:com/example/shape/Shape.java package com.example.shape; // 声明一个密封接口,只允许 Circle 和 Rectangle 实现 public sealed interface Shape permits Circle, Rectangle { double area(); } // 文件路径:com/example/shape/Circle.java package com.example.shape; // final 类,不能再被继承 public final class Circle implements Shape { private final double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // 文件路径:com.example.shape.Rectangle.java package com.example.shape; // non-sealed 类,允许被任意继承(开放了继承) public non-sealed class Rectangle implements Shape { protected final double width, height; public Rectangle(double width, double height) { this.width = width; this.height = height; } @Override public double area() { return width * height; } } // 文件路径:com/example/shape/Square.java package com.example.shape; // Square 继承自已被声明为 non-sealed 的 Rectangle,这是允许的 public class Square extends Rectangle { public Square(double side) { super(side, side); } }关键点解析:
sealed: 声明一个密封类型。permits: 列出所有允许的直接子类型。子类型必须与父类型在同一模块内,或者如果未定义模块,则需在同一包内。- 子类型修饰符:每个被许可的子类型必须用以下之一修饰:
final: 终止继承链。sealed: 继续密封,形成另一个密封层次。non-sealed: 使该类对普通继承开放,这是“解封”该分支的唯一方式。
模式匹配与密封类的强强联合:密封类与instanceof模式匹配结合时威力巨大。编译器知道所有可能的子类,因此可以检查switch表达式是否穷尽了所有情况。
public class ShapeCalculator { public static String describe(Shape shape) { // 使用模式匹配 switch 表达式(JDK 17 预览,JDK 21 转正) return switch (shape) { case Circle c -> "圆形,面积: " + c.area(); case Rectangle r -> "矩形,面积: " + r.area(); // 不需要 default 分支!因为 Shape 是密封的,编译器知道只有两种情况。 // 如果未来在 permits 中新增了 Triangle,这里编译会报错,强制你处理新情况。 }; } public static void main(String[] args) { Shape circle = new Circle(5.0); Shape rect = new Rectangle(4.0, 6.0); System.out.println(describe(circle)); // 输出:圆形,面积: 78.53981633974483 System.out.println(describe(rect)); // 输出:矩形,面积: 24.0 } }适用场景与最佳实践:
- 领域驱动设计:精确建模有固定种类实例的领域概念,如订单状态、支付方式、票据类型。
- 替代枚举:当每种“种类”需要携带大量不同数据或行为时,密封类比枚举更强大。
- 编译器辅助的完整性检查:利用编译器确保所有情况都被处理,避免运行时错误。
- 最佳实践:优先将子类定义为
final,除非你有意扩展该分支。谨慎使用non-sealed,因为它会破坏密封性带来的编译期检查优势。
5. 模式匹配:让类型检查和转换一气呵成
模式匹配旨在简化 Java 程序中常见的“检查类型-转换类型-使用”模式。它在 JDK 17 中继续演进,instanceof模式匹配已转正,switch模式匹配仍是预览特性。
5.1instanceof模式匹配(正式特性)
传统写法非常冗余:
if (obj instanceof String) { String s = (String) obj; System.out.println(s.length()); }使用模式匹配后,类型检查和变量绑定一步到位:
if (obj instanceof String s) { // 变量 s 在这里自动被定义为 String 类型,并完成了赋值 System.out.println(s.length()); } // s 的作用域仅限于 if 块内作用域精炼:模式变量s的作用域是“流作用域”。它仅在instanceof判断为真的分支中可用,并且其值就是被匹配的obj。
Object obj = "Hello, Pattern Matching!"; if (!(obj instanceof String s)) { return; } // 此时,s 在此处也可用!因为流程已经确定 obj 是 String。 System.out.println(s.toUpperCase());5.2switch表达式与模式匹配(预览特性)
这是 JDK 17 中更强大的预览特性(需添加--enable-preview编译参数)。它允许在switch中直接匹配类型并提取值。
// 使用 --enable-preview 编译运行 static String formatterPatternSwitch(Object obj) { return switch (obj) { case Integer i -> String.format("整数 %d", i); case Long l -> String.format("长整数 %d", l); case Double d -> String.format("浮点数 %f", d); case String s -> String.format("字符串 \"%s\"", s); default -> obj.toString(); }; } public static void main(String[] args) { System.out.println(formatterPatternSwitch(100)); // 整数 100 System.out.println(formatterPatternSwitch(100L)); // 长整数 100 System.out.println(formatterPatternSwitch(3.14)); // 浮点数 3.140000 System.out.println(formatterPatternSwitch("Hello")); // 字符串 "Hello" }结合密封类实现穷尽性检查:如前文Shape示例所示,当switch匹配一个密封类时,编译器可以验证是否覆盖了所有许可的子类,从而可以安全地省略default子句。这是提升代码健壮性的利器。
模式匹配的深远影响:它不仅仅是语法糖。它改变了我们处理多态数据的思维方式,让代码更专注于业务逻辑而非机械的类型操作。未来,模式匹配还将支持记录类、数组解构等更复杂的模式。
6. 文本块:告别繁琐的字符串拼接
文本块在 JDK 15 中转正,但在 JDK 17 中依然是值得强调的、能极大提升代码可读性的特性。它用于处理多行字符串,如 JSON、XML、SQL 或 HTML 片段。
传统写法的痛苦:
String json = "{\n" + " \"name\": \"张三\",\n" + " \"age\": 30,\n" + " \"city\": \"北京\"\n" + "}";文本块的优雅:
String json = """ { "name": "张三", "age": 30, "city": "北京" } """;核心规则:
- 以三个双引号
"""开始和结束。 - 结束符
"""的缩进决定了整个文本块每行被去除的前导空白符数量。以上例为例,结束符在最左列,那么文本块内每行开头的空格都会被保留(相对于结束符)。如果结束符前有空格,则会去除每行相应数量的前导空格。 - 文本块内部的行终止符会被统一为换行符
\n。 - 提供了新的转义序列:
\s:表示一个空格,防止末尾空格被去除。\:行终止符,用于将一行长内容在源码中拆分成多行,但实际字符串中不换行。
实用示例:SQL 查询
String query = """ SELECT id, name, email FROM users WHERE status = 'ACTIVE' AND created_at > ? ORDER BY created_at DESC LIMIT 100 """;最佳实践:
- 对于任何多行字符串字面量,优先使用文本块。
- 注意结束符的位置来控制缩进。
- 在需要对齐的场合(如生成表格),结合
\s和String::formatted或String::translateEscapes方法使用。
7. 新的伪随机数生成器:更清晰、更灵活的 API
java.util.random包下新增了一套全新的伪随机数生成器 API。它解决了旧Random和ThreadLocalRandomAPI 的若干问题:算法固定、扩展性差、流支持不友好。
新 API 的核心:引入了RandomGenerator接口作为所有生成器的抽象,并提供了多种算法实现。
import java.util.random.*; public class NewRandomDemo { public static void main(String[] args) { // 1. 获取默认的随机数生成器 (推荐) RandomGenerator generator = RandomGenerator.getDefault(); // 通常是 L32X64MixRandom System.out.println("Default: " + generator.nextInt(100)); // 2. 获取特定算法的生成器 RandomGenerator l128X256 = RandomGenerator.of("L128X256MixRandom"); System.out.println("L128X256MixRandom: " + l128X256.nextDouble()); // 3. 使用流 API 生成一组随机数 generator.ints(5, 1, 101) // 生成5个 [1, 100] 的整数 .forEach(System.out::println); // 4. 可跳转的生成器 (用于并行计算) JumpableGenerator jumpableGen = (JumpableGenerator) RandomGenerator.of("Xoshiro256PlusPlus"); jumpableGen.jumps() // 生成一个可无限跳转的流 .limit(3) .forEach(jumpedGen -> System.out.println("Jumped: " + jumpedGen.nextLong())); } }算法选择:新 API 提供了多种算法,各有侧重:
L32X64MixRandom:平衡性能和质量,是默认选择。L64X128MixRandom,L128X128MixRandom,L128X256MixRandom:更高质量,适用于蒙特卡洛模拟等。Xoshiro256PlusPlus,Xoroshiro128PlusPlus:非常快,但状态空间较小。SecureRandom:密码学安全的随机数。
最佳实践:
- 在大多数情况下,直接使用
RandomGenerator.getDefault()。 - 如果需要可重现的随机序列(如测试),使用
RandomGenerator.of(“算法名”)并传入固定种子。 - 在并行流中生成随机数时,考虑使用
SplittableGenerator来避免竞争。 - 对于安全敏感场景,必须继续使用
SecureRandom。
8. 强封装 JDK 内部 API:为未来做好准备
这是一个重要的兼容性突破点。从 JDK 9 引入模块化开始,JDK 内部 API(如sun.misc.*,com.sun.*下的许多类)就被强烈不建议使用。JDK 17 进一步强化了封装,默认情况下,通过反射访问这些内部 API 会受到限制。
这意味着什么?如果你或你依赖的第三方库,通过反射调用了 JDK 内部 API,在 JDK 17 上运行时可能会抛出IllegalAccessError。
如何排查和解决?
- 识别问题:运行应用时添加
--illegal-access=warn参数,JVM 会警告所有非法访问。java --illegal-access=warn -jar your-application.jar - 定位根源:警告信息会包含触发访问的栈轨迹,帮助你定位到是自身代码还是某个第三方库。
- 解决方案:
- 首选:寻找并升级到使用标准 API 的替代库或该库的新版本。
- 临时绕过:如果必须使用,可以通过命令行参数临时开放访问。但这只是权宜之计,不推荐用于生产环境。
# 开放所有模块的所有内部API(极度不推荐) java --add-opens java.base/java.lang=ALL-UNNAMED -jar your-app.jar # 更精确地开放特定模块的特定包 java --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/sun.security.util=ALL-UNNAMED -jar your-app.jar - 模块化应用:如果你将自己的应用打包为模块(使用
module-info.java),可以在模块描述文件中声明对所需内部包的opens或requires。
最佳实践:
- 在新项目中,从一开始就避免使用任何内部 API。
- 升级到 JDK 17 前,使用
--illegal-access=warn对现有应用进行充分测试。 - 积极推动依赖库的维护者更新其代码,放弃对内部 API 的依赖。
9. 其他重要特性与移除项
9.1 移除 Applet API
Java Applet 技术早已被现代浏览器淘汰。JDK 17 终于移除了相关的java.applet包。如果你的古老项目还在使用 Applet,必须将其重写为其他技术(如 Java Web Start 或直接转换为 Web 应用)。
9.2 弃用并准备移除 RMI 激活机制
远程方法调用激活机制是 RMI 中一个复杂且很少使用的部分。它已被标记为“弃用,未来移除”。大多数 RMI 应用不受影响,除非你明确使用了java.rmi.activation包。
9.3 增强的伪随机数生成器
除了新 API,原有的java.util.Random、ThreadLocalRandom和SplittableRandom现在都实现了新的RandomGenerator接口,这意味着它们可以使用新的流方法等,保持了向后兼容。
9.4 性能提升:上下文特定的反优化
JVM 的即时编译器进行了优化,减少了某些情况下不必要的全局反优化,提升了长期运行应用的性能稳定性。
10. 升级到 JDK 17:完整步骤与排查清单
将现有项目从 JDK 8 或 11 升级到 17,需要系统性的步骤。
10.1 升级前准备
- 备份:确保代码和配置有完整备份。
- 版本控制:在单独的分支(如
jdk17-migration)上进行升级操作。 - 依赖审查:检查所有 Maven/Gradle 依赖,确认其最新版本是否支持 JDK 17。重点关注:
- ASM, CGLIB 等字节码操作库。
- Lombok, MapStruct 等注解处理器。
- 日志框架、连接池、ORM 框架等核心组件。
10.2 构建工具与 IDE 配置
如前文第 3 节所述,更新构建脚本中的 Java 版本。
10.3 编译与测试
- 编译项目:运行
mvn clean compile或gradle compileJava。解决所有编译错误,可能涉及:- 使用已移除的 API:寻找替代方案。
- 废弃 API 的警告:根据建议更新代码。
- 静态代码分析:使用 IDE 的检查工具或 SonarQube 扫描,处理与 Java 17 语言级别相关的问题。
- 全面运行测试:包括单元测试、集成测试。特别注意测试那些可能涉及反射、序列化、本地方法或特定 JVM 行为的模块。
10.4 运行时验证
- 内部 API 访问:使用
--illegal-access=warn运行测试和集成环境,检查警告日志。 - 启动参数:检查现有的 JVM 启动参数。移除已废弃的参数(如
-XX:+AggressiveOpts),并考虑添加新的 ZGC/Shenandoah GC 参数进行性能测试。 - 性能基准测试:对关键接口进行压测,对比升级前后的性能指标(吞吐量、延迟、内存占用)。
10.5 部署与监控
- 分阶段部署:先在预发布/灰度环境部署,观察一段时间。
- 加强监控:重点关注 GC 日志、错误日志、应用性能指标是否有异常。
- 回滚预案:准备好快速回滚到旧版本 JDK 的方案。
11. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
编译错误:找不到符号(如sun.misc.BASE64Encoder) | 使用了被强封装的 JDK 内部 API。 | 1. 查看错误信息中的类名。 2. 使用 --illegal-access=warn运行确认。 | 使用标准 API 替代,如java.util.Base64。 |
运行时错误:java.lang.IllegalAccessError | 通过反射访问了强封装的内部 API。 | 分析错误堆栈,定位调用代码。 | 1. 升级调用该 API 的第三方库。 2. 临时使用 --add-opens参数开放访问(仅作过渡)。 |
| 应用启动变慢 | 可能触发了更多的类加载或 JIT 编译。 | 对比启动日志,检查是否有大量警告或异常。 | 1. 确保依赖库兼容。 2. 对于容器化部署,考虑使用 AppCDS 加快启动。 |
| 性能下降或内存使用异常 | 垃圾回收器行为变化或新 JIT 优化不适用于特定代码模式。 | 1. 采集并对比升级前后的 GC 日志。 2. 使用 Profiler 工具分析热点。 | 1. 调整 GC 参数(如切换到 G1 或 ZGC 并调优)。 2. 检查是否有代码依赖于未定义或特定的 JVM 行为。 |
| 第三方库不兼容 | 库在运行时动态生成字节码或使用内部 API。 | 查看该库的官方 issue 或发布说明。 | 1. 寻找替代库。 2. 等待库发布新版本。 3. 如果库是开源的,可尝试自行修补。 |
switch表达式模式匹配编译失败 | 未启用预览特性。 | 检查编译器错误信息。 | 在 Maven/Gradle 中为编译插件添加--enable-preview参数。 |
12. 生产环境最佳实践与建议
- GC 选择:对于低延迟要求的微服务,优先评估 ZGC 或 Shenandoah。对于吞吐量优先的应用,G1 仍然是稳健的选择。务必进行充分的性能测试。
- 容器化支持:确保 Docker 镜像使用专为 JDK 17 调整的基础镜像(如
eclipse-temurin:17-jre),并正确设置容器内存限制和 JVM 堆参数(使用-XX:+UseContainerSupport,该选项在 JDK 17 中默认启用)。 - 模块化考量:除非你有明确需求,否则大多数应用仍可作为“未命名模块”运行,无需立即模块化。模块化是一个架构决策,应逐步推进。
- 安全强化:利用 JDK 17 更强的默认安全设置。定期使用
jdeprscan工具扫描代码中使用的已弃用 API,制定迁移计划。 - 持续集成流水线:在 CI/CD 流水线中固定使用 JDK 17 进行构建和测试,确保代码库持续兼容。
JDK 17 不是一次激进的革命,而是一次扎实的进化。它通过密封类、模式匹配等特性让 Java 语言表达力更强,通过强封装让平台更安全,通过持续的 JVM 优化让应用性能更好。升级的过程,本质上是对项目技术债的一次清理和对未来投资。
对于新项目,毫无悬念应选择 JDK 17 作为起点。对于老项目,升级可能需要一些工作量,但带来的性能提升、安全性增强以及为未来特性(如虚拟线程)铺平的道路,使得这项投资非常值得。建议从非核心业务模块开始试点,积累经验,逐步铺开。