从Java 9到Java 25:核心新特性与升级实战解析 📅 发布时间:2026/9/18 21:28:52 👁 浏览次数: 我大概是工作第四年才真正被 Java 版本迭代给“追”着跑。以前大家聊 Java默认都是 Java 8谁要是用上 Java 11 都算激进。从 Java 9 开始Oracle 把发版节奏从“五年憋大招”改成“半年一更”Java 9、10、11……一路冲到现在的 Java 25。版本多了面试题也跟着换血什么虚拟线程、record、sealed class已经成了 JDK 新特性面试题的常客。这篇内容不是想跟你念一遍官方 Release Notes而是想从实际开发的角度把 Java 9 到 Java 25 之间真正值得用的核心新特性串一遍。你如果是一个正在准备 Java 面试的候选人或者刚接手一个老项目准备升级 JDK又或者纯粹想知道这些新版本除了版本号变快之外到底改了啥这篇文章应该能帮你在半小时内建立起一条清晰的知识线。1. 版本节奏与 LTS 策略从 Java 8 一步跨到 21 才是常态1.1 “半年一版”背后的逻辑以及为什么你不用每个版本都追Java 9 之前JCP 制定新特性的周期是以“年”为单位的Java 8 到 Java 9 中间隔着三年多。到了 Java 9整个社区的反馈是发布太慢新特性难产很多好点子因为赶不上统一发布窗口被无限推迟。所以 Oracle 改成了基于时间的发布模型Time-Based Release每六个月出一个 feature release每个版本都有一个确切的发布日期到了点就发不因为某个特性没写完而延期。做不到就砍掉放到下一个版本再说。这也解释了为什么你能看到 Java 9 的模块化、Java 10 的 var、Java 11 的 HTTP Client 这些特性是一个版本一个版本“挤”出来的。半年一版对普通开发者的直接影响是你不需要每个版本都学一遍因为大部分版本之间的增量并不大。你真正该关注的是 LTS长期支持版本也就是 Oracle 承诺提供多年稳定支持的那几个Java 8、11、17、21按节奏下一个就是 25。中间的 9、10、12、13、14、15、16、18、19、20、22、23、24更像是在为 LTS 版本做功能预演和打磨。用我自己的话说Java 9 到 25 这段演进里真正会产生“换代感”的里程碑只有三个Java 9 的模块化、Java 17 的密封类与模式匹配雏形、Java 21 的虚拟线程和 record 模式全面转正。把这几个节点吃透面试官问“JDK 新特性”的时候你就不会慌。1.2 LTS 怎么选不要站队按项目状态决定我在公司里被问过无数次“项目该用哪个 JDK”。我的建议分几种情况如果是老项目Java 8 跑得好好的业务没有扩容压力也没有新并发需求那升不升都行先把代码里用到的第三方库是否兼容 JDK 17/21 排查完再说。如果是新项目直接上 Java 17因为它是 2011 年以后语法和 API 最“现代化”的 LTSrecord、switch 表达式、文本块这些都能用IDE 和构建工具支持也最成熟。如果团队愿意折腾一点或者你正在做高并发、低延迟相关的系统Java 21 是更好的选择。虚拟线程和分代 ZGC 这两个大件已经在 21 正式转正这是 Java 并发模型和 GC 策略十年来最大的一次更新。Java 25 按三年一个 LTS 的节奏也应该成为长期支持版本但它的很多核心特性此时还在预览和孵化阶段我的态度是观望优先不急着上线。2. 语法层面的变化这些新写法能让代码量直接少三分之一2.1 var局部变量类型推断用了就回不去Java 10 最出圈的语法就是var。它的作用说白了就是让你声明局部变量的时候不用写类型编译器会根据右侧表达式自动推断。比如MapString, ListString map new HashMap();可以直接写成var map new HashMapString, ListString();或者配合钻石操作符进一步简化。但var不是 JavaScript 里的动态类型它依然是强类型编译时就已经确定具体类型类型写错照样报编译错误。真正舒服的地方在于泛型嵌套层级很深的时候比如ListMapString, ListInteger这种类型从前要写两行现在一行就能搞定。有几个典型场景我不建议用var第一个是方法返回值的变量如果方法的返回类型是接口你用var推断出的具体类型可能跟同事预期不一致第二个是用三元表达式或者 lambda 推断出奇怪类型的时候可读性会变差第三个是匿名内部类var推断出来的是匿名类不是它的父类这时候赋值给变量会有点反直觉。2.2 switch 表达式与箭头语法放弃 break拥抱 yield从 Java 14 正式转正的 switch 表达式是我最推荐的第一个“升版本动力”。以前写switch是一堆case加break漏一个 break 就会 fall-through而且整个 switch 只是“语句”不是“表达式”你没法直接把它赋值给变量。新语法长这样int days switch (month) { case 1, 3, 5, 7, 8, 10, 12 - 31; case 4, 6, 9, 11 - 30; case 2 - 28; default - 0; };每个分支用箭头-表示不会 fall-through多个 case 可以用逗号并列。如果分支逻辑复杂需要用块语句块内部用yield返回结果。这个特性最大的意义是switch 终于可以当作一个有返回值的表达式用代码表达能力和可读性都上来了。在 Java 21 之前switch 还能配合模式匹配比如case String s -这种形式可以在 case 里直接绑定变量并做类型判断这个下面单聊。2.3 文本块多行字符串终于不用拼转义了Java 13 引入文本块预览Java 15 转正。以前写 SQL、HTML、JSON要么用拼一堆换行要么写成一长串带\n的字符串肉眼根本没法校对。文本块用三个双引号包裹可以直接保留原文格式String sql SELECT id, name FROM user WHERE status 1 ;有几个细节要注意开头的后面必须换行结尾的位置决定公共缩进基准编译器会自动把每行缩进中公共的部分去掉。如果字符串内容里有需要转义成\这个很少遇到但遇到了会比较坑。我常用文本块配合.formatted()方法做模板填充比 String.format 直观得多尤其适合动态拼接 SQL、生成 JSON 报文这类场景。2.4 record数据类的正规军没 Lombok 也能体面Java 14 预览、Java 16 正式转正的record基本就是为 DTO、VO 这一类“只承载数据没有业务行为”的类量身定做的。你只需要写一行声明编译器会自动生成构造器、equals、hashCode、toString以及每个字段的读取方法public record UserInfo(String name, int age) {}这个 record 跟手写一个标准 POJO 是等价的但是代码量少了八成。它跟 Lombok 的Data最大的区别是record 是一个语义化的类型天然不可变所有字段默认final而且没有 setter。如果你需要”只读数据”的语义record 比 Lombok 更严谨。record 还有一些高级玩法比如紧凑构造器可以做参数校验public record UserInfo(String name, int age) { public UserInfo { if (age 0) { throw new IllegalArgumentException(age must be positive); } } }要注意 record 不能继承其他类也不能被继承它不是万能的只有当你确定这个类型就是“值对象”时才适合用。2.5 sealed class 与 instanceof 模式匹配面向对象的新姿势Java 17 正式转正sealed类它解决的问题是继承这个事以前要么开放给所有人要么就 final 彻底锁死缺一个中间状态。sealed 类允许你声明一个类的可继承范围超出范围直接编译报错public sealed interface Shape permits Circle, Square, Triangle {}配合 Java 16 转正的instanceof模式匹配整个分支逻辑会变得非常干净。过去写if (obj instanceof String)还得在方法体内强转一次现在可以在判断的同时直接绑定变量if (obj instanceof String s) { System.out.println(s.length()); }到了 Java 21record 模式和 switch 模式匹配也全部转正你可以这么写Object obj ...; String result switch (obj) { case null - null; case String s - String: s; case Point(int x, int y) - Point: x , y; default - unknown; };case null也被正式支持了不用再担心 switch 对 null 抛出 NPE。这套组合拳下来很多本来要写策略模式或者多态分发的代码其实用模式匹配就能做得非常紧凑。3. 核心库与并发模型虚拟线程才是 Java 21 的王炸3.1 集合和 Stream 的小改进从 of() 到 toList()Java 9 给集合接口加了一组of工厂方法创建不可变集合终于不用再绕Collections.unmodifiableList了。List.of(1, 2, 3)、Set.of(a, b)、Map.of(k, v)都很好用。注意这个方法创建的集合不能增删改传 null 会抛 NPE比较适合定义常量。Stream API 在 Java 9 加了takeWhile、dropWhile、iterate重载和ofNullableJava 16 又加了toList()。以前要把 Stream 收集成 List 必须写.collect(Collectors.toList())现在直接.toList()就行。区别在于这个新的toList()返回的是一个不可变 List而 Collectors 返回的是可变 List如果需要后续增删自己斟酌。3.2 HTTP Client终于不用再堆第三方 HttpUtil 了Java 11 把 HTTP Client 正式转正到java.net.http这是标准库对网络请求的一次大补课。以前写 JDK 原生 HTTP 请求要面对难用的HttpURLConnection所以大家才都去用 Apache HttpClient 或者 OkHttp。现在官方这个支持 HTTP/2、支持异步、支持 WebSocket基本的 GET/POST 请求写起来很顺手HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://api.example.com/user/1)) .header(Accept, application/json) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString());异步版本用sendAsync配合CompletableFuture可以做并发请求编排。这套 API 能不能完全替代 OkHttp我的经验是简单请求和内部服务调用可以考虑直接上它减少一个第三方依赖但如果你的项目重度使用拦截器、连接池调优、自定义序列化那 OkHttp 的优势还在。3.3 虚拟线程并发编程的“廉价线程”时代虚拟线程是 Java 19 预览、Java 21 正式转正的特性也是这几年 Java 并发方向最重磅的更新。它的核心思想是不再让线程一对一绑定操作系统线程而是让 JVM 自己管理成千上万个轻量级线程对象操作系统线程只作为载体在阻塞时被自动释放去执行其他虚拟线程。以前我们处理高并发 IO套路是线程池加异步回调甚至上个响应式框架。虚拟线程的出现让“每任务一线程”这种最直观的模型重新变得可行。代码不用重构只要把new Thread(...)换成Thread.startVirtualThread(...)或者用Executors.newVirtualThreadPerTaskExecutor()创建执行器try (var executor Executors.newVirtualThreadPerTaskExecutor()) { ListFutureString futures tasks.stream() .map(task - executor.submit(() - doWork(task))) .toList(); }但虚拟线程有几个坑得提前知道。第一虚拟线程不能池化池化虚拟线程等于脱裤子放屁它的设计初衷就是“用完即弃”。第二如果代码里用了synchronized块虚拟线程在阻塞时会钉住pin底层平台线程从而降低并发效果遇到这种情况建议换ReentrantLock。第三虚拟线程只适合 IO 密集任务CPU 密集任务用它没意义。3.4 结构化并发与作用域值并发代码的整洁之道Java 21 另一个重量级孵化特性是结构化并发Structured Concurrency对应包java.util.concurrent下的StructuredTaskScope。它的目标是让并发任务的生命周期和代码块的生命周期保持一致。用一个示例讲try (var scope new StructuredTaskScope.ShutdownOnFailure()) { FutureString user scope.fork(() - fetchUser()); FutureString order scope.fork(() - fetchOrder()); scope.join(); return user.resultNow() order.resultNow(); }这个写法的好处是如果任何一个子任务失败整个 scope 会立即取消剩余任务不会像裸用CompletableFuture那样出现“主线程干完了还在等后台脏线程”的问题。作用域值ScopedValue则是ThreadLocal的改进版它让线程局部变量在并发和虚拟线程场景下更安全这个特性目前还在孵化状态普通项目可以先不碰。4. 性能、GC 与 JVM 底层变快不是玄学4.1 分代 ZGC大堆降到毫秒级暂停的真相Java 11 引入了实验性的 ZGCJava 15 转正Java 21 又推出了分代 ZGC。ZGC 的核心目标是支持大堆从几十 G 到几 T同时把 GC 暂停时间控制在 10ms 甚至 1ms 以内。它的做法是并发标记、并发整理尽量把工作分摊到多个 GC 线程和业务线程同时跑。以前用 G1 应对几十 G 堆一次 Mixed GC 可能有几十上百毫秒暂停这对低延迟服务是噩梦。换上 ZGC 之后哪怕堆很大停顿也能保持在很低水位。启动参数很简单JVM 参数加上-XX:UseZGC就行JDK 21 上分代 ZGC 可以直接用-XX:UseZGC -XX:ZGenerational。不过 ZGC 不是银弹。它牺牲了一些吞吐量来换低延迟如果你的服务是批处理型、CPU 密集型G1 依然是稳妥选择。另外 ZGC 在 JDK 21 里内部也还在继续打磨生产环境用之前一定要先用你自己的真实压测场景跑一遍别光看官方 benchmark。4.2 CDS 与 AppCDS启动时间能省一大截类数据共享CDS是从 Java 5 就有的技术但直到 Java 10、12、13 才逐步默认化和增强。它的原理是 JVM 启动时把核心类库的元数据、已加载类信息做一份归档下次启动直接映射到内存减少类加载和校验开销。对单体服务来说启动时间能从十几秒降到几秒。AppCDS 更进一步允许把你的应用 jar 里的类也打进归档。配合 Java 13 的动态归档你不用手动指定 dump 类列表跑一次常见启动路径就能自动生成。Spring Boot 项目经常用的spring-boot-maven-plugin在 JDK 21 之后也能配合 CDS 做优化。这个优化对普通 web 应用提升有限但对函数计算、短生命周期任务这种启动敏感型场景非常有价值。4.3 Foreign Function Memory APIJNI 的现代化替身Java 22 正式转正的java.lang.foreign外部函数和内存 API解决了 JNI 和ByteBuffer被诟病多年的问题JNI 写起来繁琐、性能损耗大ByteBuffer管理和内存释放又别扭。新 API 允许你直接调用本地方法、操作堆外内存语法更安全性能也更接近原生。Linker linker Linker.nativeLinker(); SymbolLookup stdlib linker.defaultLookup(); MethodHandle strlen linker.downcallHandle( stdlib.find(strlen).orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) );它和 Panama 项目相关目标就是把 JNI 从“难写到不想写”变成“正常 Java 代码里的一块”。一般业务开发暂时用不上但如果你在做嵌入式、图形库绑定、高性能存储引擎这个方向非常值得关注。4.4 Valhalla 与紧凑对象头未来 Java 对象的两种“瘦身”Java 25 这个时间点最值得关注的长期方向是 Valhalla 项目。它要解决的核心问题是Java 对象在内存里的开销太大每个对象都有对象头mark word、class pointer压缩后也有 12 字节甚至更多一个频繁使用的 Integer 对象可能比一个原始 int 多占好几倍内存。Valhalla 的路线是引入“值对象”和“原始类型”让自定义类型可以像 int 一样扁平分布在数组里而不是到处是引用和指针。Java 24 引入了紧凑对象头Compact Object Headers作为实验性特性目的就是把对象头进一步压缩。这些改动如果真正落地对大数据量、缓存高命中的系统是巨大的收益。不过这些目前大部分还在预览和孵化阶段普通开发者知道有这么回事就行不用深入研究。5. 升级 JDK 之前必须解决的兼容性与构建问题5.1 编译字节码版本Java 8 的老代码真的能直接跑在 21 上吗升级 JDK 最大的疑问就是老代码能不能跑。Java 官方强调二进制兼容性意思是只要你的代码在 Java 8 编译出的 class 文件字节码版本是 52.0理论上可以被更高版本的 JDK 加载执行。我在实践中也验证过很多老项目ClassNotFoundException 或 NoSuchMethodError 大多不是字节码兼容性问题而是用了第三方库里的旧 API。但有个反方向要特别注意你用 JDK 21 的javac编译代码时默认生成的字节码版本就是 65.0老版本的 JVM 跑不了。如果想发布的 jar 能兼容旧运行环境编译时需要加--release参数。比如用 JDK 21 编译出 Java 17 字节码javac --release 17 Main.java这个参数比-source和-target更安全因为它会同时限制编译期 API 的可用范围防止你不小心用到新版本才有 API 却声明兼容低版本。5.2 module-path 和 classpathJava 9 模块化带来的第一个大坑Java 9 最大的架构变化是模块化JPMS以前整个 JDK 是一个巨大的rt.jar现在被拆成了很多java.*和jdk.*模块。如果你的老项目用的是 classpath 方式其实还好影响不大。真正中招的是某些框架或者工具它们用了--add-opens或--add-exports来绕过模块封装。比如 Spring 框架早期在 Java 17 上跑就要额外加一堆 JVM 参数放开反射访问权限因为 Java 17 开始强封装 JDK 内部 API。新项目如果在启动时报IllegalAccessError或InaccessibleObjectException第一反应就去看官方文档对 JDK 17/21 的兼容说明通常都会有对应的--add-opens参数。我的经验是简单项目可以直接用 classpath不用强行模块化。只有当你确定要把一个大型系统拆成可复用、有清晰边界的组件时modularity 才值得投入。5.3 Maven 和 Gradle 的版本配置别让构建工具拖后腿项目升级到 JDK 17 或 21第一件事不是改代码而是把构建工具升级到支持新字节码版本的版本。Maven 3.8 以上、Gradle 7.3 以上是对 Java 17 支持比较舒服的底线。Gradle 对 JDK 版本支持更敏感老版本 Gradle 跑在 JDK 21 上会直接报Unsupported class file major version。pom.xml 里推荐直接这样配置properties maven.compiler.release21/maven.compiler.release /properties用release而不是sourcetarget原因前面说过它可以同时限制 API 权限不会有“编译过了但运行时 NoSuchMethodError”这种幺蛾子。另外JDK 9 之后maven-compiler-plugin的默认版本如果太老也可能出问题建议顺手升级到 3.11.0 以上。5.4 第三方库兼容清单动手前先做一次体检老项目升级到 Java 17 或 21最容易卡住的是第三方库尤其是字节码增强和反射重灾区Lombok、CGLIB、Mockito、ByteBuddy、各种 Agent。Lombok 老版本在 JDK 16 之后经常直接启动失败报java.lang.ExceptionInInitializerError解决办法是升级到 1.18.30 以上。Mockito 需要 4.x 以后。CGLIB 基本被 ByteBuddy 替代。我升级前一般会做一个五分钟的体检把项目所有依赖导出来对照官方兼容矩阵查一遍再把测试跑起来重点看代理类生成、序列化、反射相关的用例。别迷信“看起来能编译”别把测试阶段的问题留到线上。6. 面试高频考点与学习路线参考6.1 面试官爱问的 Java 8 到 21 变化无非这几类现在 Java 面试题里“JDK 8 新特性”已经是常规操作了卷一点的岗位会追问 JDK 11、17、21 的新东西。面试官真正想听的并不是你背了多少版本号而是你能不能说出某个新特性解决了什么实际问题。比如问record和 Lombok 的区别答案点在于record 是语言级不可变类型语义更严谨序列化和结构化使用更自然Lombok 是编译期注解处理器灵活但屏蔽了真实代码。比如问switch表达式和旧switch的区别重点说 fall-through、箭头语法和返回值能力。再比如问虚拟线程和线程池的区别重点说虚拟线程不绑定 OS 线程、不能被池化、适合高并发 IO 场景。我把常见考点整理了一下考点核心回答思路Java 8 和 Java 17 的主要变化加上记录、sealed、模式匹配、switch 表达式、HTTP Client去掉PermGen 改为 Metaspace为什么说 Java 21 是分水岭虚拟线程转正、record 模式转正、分代 ZGC 可用var 能用在哪些位置局部变量不能用于字段、方法参数、返回类型sealed class 解决了什么问题限制继承范围让穷尽式分支更安全CDS 优化原理类加载结果归档启动时映射减少类加载开销ZGC 与 G1 怎么选追求低延迟大堆用 ZGC吞吐量敏感用 G1默认还是 G16.2 虚拟线程和线程池面试题怎么答虚拟线程相关题目在 2024 年之后几乎成了必问项。面试官常问“虚拟线程和平台线程有什么区别”“虚拟线程能替代线程池吗”你要抓住三个要点第一平台线程一对一映射到 OS 线程数量受系统资源限制线程切换成本高。虚拟线程由 JVM 调度映射到少量 OS 线程上数量可以非常大创建销毁成本低。第二虚拟线程适合 IO 密集型任务因为阻塞时 JVM 可以腾出载体线程执行别的虚拟线程不适合 CPU 密集型。第三不要池化虚拟线程每任务直接创建一个即可这和平台线程的池化思路完全相反。可以再补一个亮点遇到 synchronized 时虚拟线程可能 pin 住载体线程所以高并发代码里优先用java.util.concurrent.locks.ReentrantLock。这个细节说出来面试官基本就能确定你是真做过功课的。6.3 一条可以直接参考的 Java 学习路线如果你刚入门 Java我的建议是先学 Java 基础语法和面向对象把 JDK 8 的 lambda、Stream、Optional 这些高频 API 用好。有一定基础后按三条线扩展并发线学 JUC、线程池、虚拟线程JVM 线学内存结构、GC 策略、调优参数框架线学 Spring Boot 生态和微服务。新特性不用按版本一个个刷跟着 LTS 版本走就好先补 Java 17 的语法再补 Java 21 的虚拟线程和结构化并发最后了解 24、25 的 Valhalla 方向。我自己带人的时候还会要求他们每种新特性都写一个能跑的最小 demo光看文章不如亲手敲一遍敲完你能真正理解怎么写才优雅、怎么写会踩坑。7. 最后说点我自己的体会和一个小建议我在实际项目里踩过几次坑之后逐渐形成了自己的一套升级策略。每个新 LTS 发布后我不会立刻改线上版本但会在个人项目和新的小模块里先试用。Java 17 我用了大概一年才敢把核心服务升上去Java 21 的虚拟线程我也是先在低峰期的业务接口里观察了几周确认没有内存和线程异常才铺开。如果你现在还在 Java 8 上犹豫我建议可以先把编译和目标版本切到 17代码先不改跑一遍测试看看兼容性。往往你会发现比想象中顺利因为大部分问题都在构建工具和 IDE 层面真正写到业务代码里的新特性反而可以慢慢加。等你在实际开发里用过record和switch表达式之后再回头看 Java 8 的样板代码大概率就不想回去了。