Java 22 出来有一阵子了但我在帮团队做技术咨询时发现一个很有意思的现象很多人还在 JDK 8 上稳如泰山哪怕 Java 21 都已经是 LTS 了Java 22 这种非长期支持版本更是直接被忽略。说实话这个心态我能理解毕竟线上跑得稳比什么都重要。但如果你愿意花二十分钟把 Java 22 的清单过一遍你会发现它其实是一个信息量很大的版本——既有直接落地的正式特性也有几个非常值得提前上手的预览功能。这篇文章我就按自己的视角把 Java 22 从正式特性到预览特性再到从 JDK 8 迁移的实操路径完整梳理一遍。先说结论Java 22 不是那种“必须立刻升级”的版本但它把一些你已经等了很久的东西推到了终点线前。外部函数与内存 API 转正、未命名变量转正、G1 区域固定、多文件源码启动这四个是我认为最有“落地感”的正式特性。字符串模板、Stream Gatherers、结构化并发、作用域值这些预览特性则是判断未来三五年 Java 写法的风向标。下面我一个个拆开讲。1. 从 JDK 8 到 Java 22先搞清楚这中间发生了什么1.1 Java 22 在版本节奏里的位置很多团队对 Java 版本的认知还停留在“JDK 8 之后就是 JDK 11”实际上 Oracle 在 2017 年之后就把发布节奏改成了半年一个版本。这意味着 Java 22 是 2024 年 3 月发布的常规版本它前面是 Java 212023 年 9 月的 LTS后面是 Java 252025 年 9 月的下一个 LTS。所以正确理解 Java 22 的方式是它不是一个“长期支持版本”而是为 Java 25 做技术和生态铺垫的过渡版本。这个定位很重要因为它决定了你该怎么用 Java 22。如果你在纠结“要不要把生产环境从 JDK 8 升到 Java 22”我的建议是别急等 Java 25 或者直接上已经成熟的 Java 21 更稳妥。但如果你在做新项目、写工具类、研究新写法Java 22 就是很好的试验场。预览特性在 Java 22 里的状态往往已经相当完善你可以提前体验并反馈问题等它们转正的时候你已经是老手了。提示区分 LTS 和非 LTS 很简单LTS 版本会有“Premier Support”和“Extended Support”的完整时间线非 LTS 只有六个月左右的更新窗口。生产环境追求的是稳定长期维护所以绝大多数团队应该以 LTS 为基线。1.2 为什么很多人还停在 JDK 8每次聊新特性都绕不开“还有人用 JDK 8”这个话题。你说 JDK 8 老吗从语言特性上看确实老但它有一个无可替代的优势生态兼容性极好。很多老项目的第三方框架、中间件客户端、自研框架都只在高版本 JDK 上测试过升级一旦出问题排查成本非常高。再加上 JDK 8 的垃圾回收、内存模型、启动速度等基础能力其实并不差很多业务系统根本用不到新特性带来的性能提升自然没有升级动力。但从另一个角度看JDK 8 之后其实积累了大量值得迁移的特性Records、sealed class、switch 表达式、文本块、虚拟线程、模式匹配等。这些不是花架子而是能实实在在减少代码量、降低出错概率的东西。我在后面的章节里会专门讲从 JDK 8 迁移到 Java 22 的路径这里先提醒一句如果你所在团队已经有两年以上没动过 JDK 版本至少应该把 Java 21 或 Java 22 放进技术规划里而不是继续无视。2. 可以直接上生产的正式特性2.1 外部函数与内存 APIJEP 454外部函数与内存 API 在 Java 22 里转正了这是我个人认为 Java 22 最重要的一个特性。它把 Java 与本地代码C/C 库互操作的门槛大幅降低以前你只能靠 JNI写起来极其痛苦要写 C 头文件的 Java 映射、要管理 native memory 的生命周期、还要处理各种类型转换。现在有了 FFM API你可以在纯 Java 代码里声明外部函数、分配堆外内存、直接读写 native 数据结构。举个例子你想调用 C 标准库的strlen函数以前用 JNI 要写一整套 C 代码和native方法声明现在只需要这样import java.lang.foreign.*; import java.lang.invoke.*; public class FfmDemo { public static void main(String[] args) throws Throwable { Linker linker Linker.nativeLinker(); SymbolLookup stdlib linker.defaultLookup(); MethodHandle strlen linker.downcallHandle( stdlib.find(strlen).orElseThrow(), FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS) ); try (Arena arena Arena.ofConfined()) { MemorySegment str arena.allocateFrom(hello); long len (long) strlen.invoke(str); System.out.println(len len); } } }你看没有一行 native 方法没有 JNI 头文件直接在 Java 里完成了对本地函数的调用。这个 API 底层做了很多安全设计比如Arena负责内存生命周期管理MemorySegment保证访问不会越界越界会抛异常而不是直接 crash。如果你们项目里有一些算法库、硬件 SDK 或者系统调用需要对接FFM API 是一个非常值得投入的方向。这里有几个实际落地时的注意点Arena 的作用域要设计好ofConfined表示单线程访问ofShared可以多线程访问但后者有额外开销能不用就不用。MemorySegment 的访问是有 checked exception 的invoke会抛Throwable封装时要统一处理。FFM API 虽然转正了但不同 JDK 小版本之间仍可能有细微调整依赖它做底层框架的团队要盯紧 release notes。2.2 未命名变量与模式JEP 456这个特性看起来小但实际用起来非常舒服。以前你写try-catch的时候catch (Exception e)里的e往往根本用不到写 lambda 的时候(a, b) - a b里的某个参数也可能用不到。未命名变量允许你用下划线_来表示“我不关心这个值”编译器不会为你生成多余的变量代码意图也更清晰。举个例子以前遍历集合并只关心 key 时你得写MapString, Integer map Map.of(a, 1, b, 2); map.forEach((key, value) - System.out.println(key));现在可以写map.forEach((key, _) - System.out.println(key));_就是“我不在乎这个 value”。还有一个典型场景是异常处理try { int result riskyOperation(); System.out.println(result); } catch (NumberFormatException _) { System.out.println(not a number); }注意这种情况下_没有绑定任何变量所以你不能再写_.getMessage()。如果你真的需要异常对象就用正常变量名如果你只是想跳过它_会让代码干净很多。除了一般的变量场景未命名模式还可以用在 instanceof 和 switch 模式匹配里。比如你只想判断某个对象是不是String类型但不需要取出它的值if (obj instanceof String _) { System.out.println(its a string); }这个特性转正后最大的价值不是性能提升其实几乎没有性能差异而是可读性和代码整洁度。审查代码时看到_就能立刻知道“哦这里作者不关心这个值”比看变量名猜意图要省力得多。2.3 G1 区域钉扎JEP 423G1 区域钉扎这个特性说实话大部分业务开发不需要关心但它对某些低延迟系统的影响非常大。它解决的是 G1 垃圾回收器在处理 JNI Critical 区域时的停顿问题。以前 GC 遇到 JNI critical 区域会直接暂定现在可以更精细地处理避免因为一个线程进入了 native 代码而让整个 JVM 的 GC 暂停时间变长。如果你的系统没有用到 JNI 或者 FFM API这个优化感知不到但如果你们有本地图像处理、加密算法调用、高性能计算之类的场景Java 22 的 G1 在混合场景下的表现会比之前平滑一些。这不是那种“一升级就快了”的特性而是“问题场景下确实变稳了”的特性。2.4 多文件源码程序启动JEP 458这个特性非常接地气。以前你想直接运行一个简单的 Java 程序如果它依赖另一个.java文件里的类你就得先javac编译再java运行或者用构建工具。Java 22 允许你直接用java命令运行由多个.java文件组成的程序它会自动编译并处理依赖关系。比如你有两个文件Hello.javapublic class Hello { public static void main(String[] args) { System.out.println(Utils.greet(java 22)); } }Utils.javapublic class Utils { static String greet(String name) { return hello, name; } }以前要这样javac Hello.java Utils.java java Hello现在只需要java Hello.javajava命令会自动解析Hello.java里的引用找到同目录下的Utils.java并编译运行。这对写脚本、做原型验证、跑测试示例来说实在太方便了。我自己写技术 demo 的时候经常就是几个.java文件在一个目录里直接java Main.java就完事完全不用碰 Maven 或 Gradle。但要注意这个模式适用于“简单程序”如果你引入了外部 jar 包依赖还是得老老实实用--class-path或者构建工具。多文件源码启动的目的不是替代构建工具而是让 Java 在“脚本化”场景下更好用。3. 值得提前上手的预览特性3.1 字符串模板JEP 459第二次预览字符串模板是很多 Java 开发者的“有生之年”系列。以前拼字符串要么用连接要么用String.format要么用MessageFormat各有各的别扭。Java 22 的字符串模板提供了一种全新的、可定制的方式。最基础的用法是String name world; String message STR.hello, \{name}; System.out.println(message); // 输出hello, world注意STR是一个模板处理器\{name}是嵌入式表达式它可以直接访问局部变量、调用方法、甚至写三元表达式int score 85; String result STR.成绩\{score 60 ? 及格 : 不及格};这比String.format直观太多也比拼接防错太多。更重要的是字符串模板的模板处理器是可以自定义的。你可以写一个JSON处理器让嵌入表达式自动做 JSON 转义可以写一个SQL处理器自动处理 SQL 参数甚至可以写一个日志脱敏处理器。这种可扩展性才是字符串模板真正的杀手锏。不过要提醒一下字符串模板目前还是预览特性需要在编译和运行时加--enable-preview参数javac --release 22 --enable-preview Demo.java java --enable-preview DemoAPI 在预览阶段可能会调整我见过有人在 Java 21 里写的STR用法到 Java 22 里语法没变但模板处理器的接口有变动。所以生产环境别用预览特性但自己研究新语法时完全可以试。注意预览特性的核心价值是收集社区反馈API 变化是正常现象。如果你想在团队内推广某个预览特性一定要提前确认 IDE、构建工具的版本是否兼容并且做好“后续版本可能调整代码”的心理准备。3.2 Stream GatherersJEP 461预览Stream API 是 Java 8 带给大家最成功的东西之一但它的一个明显短板是中间操作的类型比较固定map、filter、limit、distinct、sorted就这些很多自定义逻辑你得靠collect到 List 再处理或者写一堆流水账代码。Stream Gatherers 要解决的就是这个问题允许你定义自定义的中间操作。举个例子我想实现一个“去重但保留第一次出现的 key”的操作以前要写ListPerson distinctByName persons.stream() .filter(distinctByKey(Person::name)) .toList();其中distinctByKey是你自己封装的 Collector 或者工具方法本质上还是借助了一个可变集合。用 Stream Gatherers 之后可以更声明式地定义一个中间操作StreamPerson distinctByName persons.stream() .gather(distinctBy(Person::name)); // 自定义一个 gatherer static T, R GathererT, ?, T distinctBy(Function? super T, ? extends R keyExtractor) { return Gatherer.of( () - new HashSetR(), (state, element, downstream) - { R key keyExtractor.apply(element); if (state.add(key)) { downstream.push(element); } return true; }, (state, downstream) - {} ); }这个Gatherer接口其实比乍看起来强大得多它支持有状态、有界、无界、可组合的中间操作。比如窗口操作每 N 个元素为一个分组、滑动窗口、按条件切分都可以用 Gatherers 实现。Java 22 的 Stream Gatherers 目前是预览特性但底层接口设计已经比较稳定我个人建议对 Stream 操作有一些进阶需求的人提前去看看java.util.stream.Gatherer的 javadoc因为一旦转正它会成为编写可复用流式逻辑的标准方式。3.3 结构化并发与作用域值JEP 462 / JEP 464都是第二次预览这两个特性放在一起说因为它们共同指向一个方向让并发代码更清晰、更可控。结构化并发的核心思想是把一组相关的并发任务放在同一个代码块里管理要么全部成功要么统一取消避免线程“泄漏”到任务生命周期之外。举个例子以前并发调用两个服务时你可能会用CompletableFuture加allOf或者自己维护ExecutorService和 Future 列表取消、异常处理、线程池关闭都要手动处理。用StructuredTaskScope之后代码结构变成了这样try (var scope new StructuredTaskScope.ShutdownOnFailure()) { FutureString user scope.fork(() - fetchUser()); FutureString order scope.fork(() - fetchOrder()); scope.join(); scope.throwIfFailed(); String result STR.\{user.resultNow()} \{order.resultNow()}; System.out.println(result); }这段代码里scope的生命周期就是 try-with-resources 块的生命周期。无论两个子任务是否完成只要代码块退出scope 就会统一处理未完成的任务。ShutdownOnFailure意味着任何一个子任务失败时其他任务会被取消这种“要么全成功要么全失败”的语义非常符合很多业务场景。作用域值Scoped Values则是用来解决线程间传递上下文的问题。以前你在线程间传 requestId、用户信息一般得靠 ThreadLocal但 ThreadLocal 在虚拟线程和池化线程场景下有很多坑比如内存泄漏、父子线程上下文不一致。Scoped Values 允许你在一段代码块里绑定一个不可变的值在这个代码块及其调用的子任务中可读但超出范围就自动失效。private static final ScopedValueString REQUEST_ID new ScopedValue(); public void handle(Request request) { ScopedValue.where(REQUEST_ID, request.id()) .run(() - { // 在整个调用链里都能读 REQUEST_ID String rid REQUEST_ID.get(); // 处理业务 }); }结构化并发 作用域值 虚拟线程Java 21 转正这套组合是未来 Java 高并发服务端编程的主流形态。Java 22 对这两个特性做了进一步打磨虽然还没转正但已经非常接近最终形态。3.4 类文件 APIJEP 457预览类文件 API 是想替代 ASM、Byte Buddy 这类字节码操作库的官方方案它的目标是提供一套标准的、安全的、可扩展的 Java 类文件解析与生成 API。为什么要做这个因为直接解析.class文件的二进制格式太容易出错而且随着每次 Java 版本更新字节码指令和属性都会变化第三方库需要不停跟进。类文件 API 的用途很广字节码增强、AOP 框架、代码分析工具、编译器的后端甚至一些代码生成器。Java 22 里它是预览状态接口大体框架已经定型但我不会建议你在生产环境的框架里直接依赖它毕竟还没稳定。如果你是做构建工具、APM 这类基础设施软件的可以提前研究一下它的设计思路未来转正之后很有可能会成为标准方案。4. 从 JDK 8 迁移到 Java 22 的实操路径4.1 迁移前先做依赖体检聊了这么多新特性肯定有人心动了但我也要泼一盆冷水从 JDK 8 直接跳 Java 22不是把java命令版本换一下那么简单。第一步要做的是依赖体检把项目里所有第三方库列出来逐个确认它们是否兼容高版本 JDK。最容易出问题的是这几类基于反射的框架比如老的 Spring 版本、老的 MyBatis 版本可能因为模块化系统或者强封装 JDK 内部 API 而挂掉。字节码操作库ASM、CGLIB、Byte Buddy 的低版本可能不支持 Java 22 的 class 文件版本。内存直接操作类库比如 Netty、RocketMQ 的旧版本可能用到已经移除的sun.misc.Unsafe行为。你可以在本地用一个不跑流量的实例先升级 JDK启动应用看日志逐项排查IllegalAccessError、NoClassDefFoundError、UnsupportedClassVersionError之类的报错。注意高版本 JDK 默认会启用更强的封装所以--add-opens、--add-exports参数可能还得加上但这些参数是治标不治本能不用尽量别用。4.2 切换语言层面的写法依赖体检通过之后就可以逐步把代码迁移到新写法。从 JDK 8 到 Java 22语言层面的变化非常多我不建议一次性全面改造而是分阶段进行第一阶段是低风险语法替换比如List.of、Map.of替代Collections.unmodifiableListvar替代显式类型switch表达式替代传统 switch。这些改动不影响运行逻辑但可以让代码先适应新风格。第二阶段是 Records 和 sealed class。你会发现很多原本要写 getter/setter/equals/hashCode 的 POJO 类用 Record 直接一行搞定而且equals和hashCode是语言层面自动生成的不会像 Lombok 那样有版本兼容问题。第三阶段才是接触虚拟线程、结构化并发、FFM API 这类底层能力。这部分要挑场景比如你是 IO 密集型服务可以把线程池换成虚拟线程来跑一版压测对比如果业务里有很多外部函数调用再评估 FFM API 的可行性。总之先低风险后高风险每步都有明确的验证指标。4.3 性能与监控上的变化JDK 8 到 Java 22 的 GC 选项变化很大。JDK 8 默认是 ParallelGCJava 22 默认是 G1其实 Java 9 就默认 G1 了而且 G1 在很多场景下表现更稳定。如果你的应用是老式大堆内存可能还需要额外测试 ZGC 的表现。这里我实测下来的体感是JDK 8 换到 Java 21/22 之后同样的服务在低延迟场景下 G1 的停顿控制通常比 ParallelGC 好但吞吐量不一定更高。所以性能调优要基于你业务的真实指标不要只看 GC 日志里的平均值。监控方面JFRJava Flight Recorder在 Java 22 里已经是内置的能力而且有更多事件类型比如对 FFM API 分配内存的监控、虚拟线程的调度统计等。JDK 8 时代你要排查线上问题还得靠jmap、jstack手动抓快照现在直接用 JFR 持续录制出问题时能回放很多细节。这一块变化虽然不是“语言新特性”但绝对是实际运维效率提升的大头。5. 常见问题与避坑实录5.1 预览特性在 IDE 里的坑预览特性最大的坑不是语法本身而是工具链的配合。IDEA 和 Eclipse 通常要设置--enable-preview才能识别预览语法而且不同 IDE 版本对新预览特性的支持程度不一样。我遇到过一次在命令行里能正常编译运行放到 IDEA 里就报“cannot find symbol STR”最后发现是 IDE 的 language level 没调到 22而且 project structure 里没有勾选 Preview features。这里分享一个排查顺序先确认java -version再确认javac --release 22 --enable-preview能通过最后再去调 IDE 的设置。5.2 字符串模板的 API 变动字符串模板在 Java 21 到 Java 22 之间做了一些调整最明显的是模板处理器的接口命名和结构变化。如果你在网上查资料写代码一定要看发布版本对应的文档不要拿着 Java 21 的示例直接在 Java 22 上跑。我在 Java 22 上实测STR的基本用法是没问题的但如果你用了自定义模板处理器或者FMT建议仔细看一下 JEP 459 的更新记录。预览特性就是这样有可能在转正之前改几轮。5.3 迁移后运行内存“变小”的错觉有些团队从 JDK 8 迁到高版本后发现同样的-Xmx设置下应用更容易 OOM于是怀疑新版本 JDK 有内存泄漏。其实很可能是默认 GC 和类元数据空间的差异导致的。JDK 8 的 PermGen 变成了 Metaspace默认上限更多受本机内存限制G1 的 region 划分和对象分配方式也和老 GC 不同。建议迁移时不要照搬老的 JVM 参数先删掉明显过时的参数比如-XX:PermSize然后用-Xlog:gc*观察实际内存行为再针对性地调整。我把这几个坑整理成了一个速查表方便你对照问题可能原因处理方式编译报invalid flag: --enable-previewjavac 版本过低或没有指定--release 22升级 JDK 并显式指定--release 22运行时提示Unsupported class file major version使用了旧版构建工具或 IDE升级 Maven/Gradle/IDE确保支持 Java 22 字节码STR找不到符号未开启预览特性或 IDE language level 不对加上--enable-preview检查 IDE 设置老框架反射报错InaccessibleObjectExceptionJDK 模块化强封装内部 API升级框架必要时临时加--add-opens过渡同样的堆配置下老出 OOMGC 默认值变化G1 行为不同于 ParallelGC重新评估 GC 参数用 JFR 观察真实分配速率应用启动变慢或发生 Full GC类元数据空间释放策略不同检查 Metaspace 参数必要时调整MaxMetaspaceSize5.4 还有几个容易被忽略的小点Java 22 里类文件版本号是 66如果你的项目用 Maven 的source/target还是 1.8那编译出来的 class 文件依然是 Java 8 格式运行在高版本 JDK 上也吃不到任何新特性这一点经常被忽略。很多人以为“JDK 装成 22 就等于用上了 22”其实编译器参数没改的话代码还是老味道。另外Java 22 对安全库也有一些更新比如默认禁用了过时的加密算法部分 TLS 相关配置有变化。如果你们是老系统升级后可能出现 SSL 握手失败之类的问题要提前做好兼容性测试。个人实操中的一点体会把这十几个特性过一遍之后我自己最关注的是结构化并发和作用域值的组合。虚拟线程帮你解决了“高并发连接数”的问题但并发代码的编排、取消、异常传播这些事还是得靠结构化并发这样的工具来理清。这两者结合能把服务端并发的代码复杂度降一个量级。至于外部函数与内存 API如果不是做底层中间件的团队可以先了解不用急着引入。Java 22 这个版本务实一点的用法是把它当作 Java 25 的“预习课”先把你在意的预览特性跑一遍积累经验等下一个 LTS 到来时你就是团队里最有发言权的那个人。最后再分享一个小技巧你不需要等公司统一升级 JDK完全可以先在自己电脑上装一个 JDK 22把日常写的小工具、算法题、demo 都跑在新版本上。等到你对这些新特性有了体感再推动生产环境升级时你说话的分量会完全不同。
阅读完成 · 觉得有帮助?