编程 Java 26 深度实战:Lazy Constants 干掉双重检查锁、HTTP/3 客户端开箱即用、原始类型模式匹配杀疯了——10 个核心 JEP 一次讲透

2026-08-16 02:14:21 +0800 CST views 10

Java 26 深度实战:Lazy Constants 干掉双重检查锁、HTTP/3 客户端开箱即用、原始类型模式匹配杀疯了——10 个核心 JEP 一次讲透

2026 年 3 月 17 日,JDK 26 正式 GA。作为 Java 六个月发布节奏下的又一个特性版(feature release),它没有像 JDK 25 那样戴上 LTS 的帽子,却在语言层面、标准库层面和 JVM 层面一口气交付了 10 个 JEP。本文不堆砌 Release Notes,而是以程序员视角,把其中真正能改变你日常写法的几个 JEP 拆开揉碎:Lazy Constants 怎么让"延迟初始化的 final"成为现实、HTTP/3 客户端为什么一行就能切、原始类型模式匹配凭什么终结 switch 的"只认引用类型"时代、结构化并发第六次预览又收敛了什么,以及 AOT 对象缓存终于能和 ZGC 同框。每个点都配可运行代码,并在最后给出一份面向生产的升级 checklist。

背景介绍:为什么说 JDK 26 是"被低估的特性版"

先对齐几个事实,避免被版本号绕晕:

  • Java 从 JDK 9 起固定六个月一个特性版(3 月 / 9 月),长期支持版(LTS)大约每两年一个。JDK 21(2023-09)和 JDK 25(2025-09)是最近的 LTS,下一个 LTS 是 JDK 29(2027-09)。
  • JDK 26 于 2026-03-17 GA,是 JDK 25 之后的非 LTS 特性版。它承接了一批在 JDK 23/24/25 反复预览、打磨的特性(模式匹配、结构化并发、Lazy Constants 前身 StableValue、PEM 等),把它们继续向前推。
  • 本次全部 10 个 JEP 一览:
JEP标题类别状态
500Prepare to Make Final Mean Final语言(准备性)交付
504Remove the Applet API类库清理交付
516Ahead-of-Time Object Caching with Any GCJVM / GC交付
517HTTP/3 for the HTTP Client API类库(网络)交付
522G1 GC: Improve Throughput by Reducing SynchronizationJVM / GC交付
524PEM Encodings of Cryptographic Objects(Second Preview)安全类库第二次预览
525Structured Concurrency(Sixth Preview)并发类库第六次预览
526Lazy Constants(Second Preview)语言/类库第二次预览
529Vector API(Eleventh Incubator)类库( incubating)第十一次孵化
530Primitive Types in Patterns, instanceof, and switch(Fourth Preview)语言第四次预览

可以看到一个明显趋势:Amber 项目(语言精致化)和 Leyden 项目(启动/预热优化)是两条主线。模式匹配、Lazy Constants 属于让代码更短更安全的 Amber 路线;AOT 对象缓存、Vector API 属于让 Java 跑得更快更稳的 Leyden/Panama 路线。下面按"值不值得你立刻上手"的优先级逐个拆解。

阅读提示:带"Preview / Incubator"的 JEP 需要显式开启。预览语言特性编译/运行加 --enable-preview --source 26;孵化模块(Vector API)运行加 --add-modules jdk.incubator.vector。生产环境请务必评估转正节奏(见文末 checklist)。


一、Lazy Constants(JEP 526):终于有"延迟初始化的 final"了

核心概念:final 的两难

《Effective Java》第三条就劝你"优先使用不可变性"。Java 手里最趁手的不可变工具是 final 字段——但 final 有个死规矩:必须在构造期(实例字段)或类初始化期(静态字段)一次性赋值,而且初始化顺序被源码书写顺序绑定。

这带来两个真实痛点:

  1. 启动拖累:一个类加载时就要把所有可能用到的重量级对象(配置、连接池、缓存)全部 final 初始化好,哪怕这次请求根本用不到。
  2. 时间耦合:A 依赖 B,B 依赖 A,你不得不上"惰性初始化 + 双重检查锁(double-checked locking)"那套样板代码,既丑又容易写错(忘了 volatile 就埋雷)。

JEP 526 给出的解法是 LazyConstant:一个持有不可变数据的对象,但初始化时机由你决定,同时 JVM 把它当成真正的常量来优化(享受和 final 同等的常量折叠、逃逸分析红利)。

架构分析:为什么它比"自己写懒加载"强

Lazy Constants 的底层机制与 JVM 内部的 @Stable 注解同源——HotSpot 早就在用类似的"稳定值"优化内部代码。JEP 526 把它以受控 API 暴露出来,并保证:

  • 至多初始化一次,多线程下也安全(内部已处理可见性与竞态,你不用自己加锁);
  • 计算结果为 null 会被拒绝(与不可变集合、ScopedValue 的语义一致,顺便换来性能);
  • 相比 JDK 25 的 StableValue(JEP 502),JDK 26 把 API 重命名为 LazyConstant,砍掉 orElseSet / setOrThrow / trySet 等偏底层的写入方法,只保留"吃一个计算函数"的工厂方法,并把集合的懒加载工厂挪进了 java.util.List / java.util.Map 接口,整体更聚焦"高频用例"。

代码实战

最经典的用法——替代双重检查锁的单例与配置加载:

import java.lang.LazyConstant;

public final class AppConfig {

    // 类加载时不再立即读盘;首次 get() 才计算,之后恒定
    private static final LazyConstant<AppConfig> INSTANCE =
        LazyConstant.of(AppConfig::loadFromDisk);

    private final String region;
    private final int maxConnections;

    private AppConfig(String region, int maxConnections) {
        this.region = region;
        this.maxConnections = maxConnections;
    }

    private static AppConfig loadFromDisk() {
        // 假设这里有真实 IO / 解析逻辑
        System.out.println("loading config (once)...");
        return new AppConfig("ap-east-1", 64);
    }

    public static AppConfig instance() {
        return INSTANCE.get(); // 线程安全、只算一次
    }

    public String region()  { return region; }
    public int maxConnections() { return maxConnections; }
}

把上面的 AppConfig 和"经典双重检查锁单例"对比,你会发现 Lazy Constants 版本:

  • 没有 volatile 字段;
  • 没有 if (instance == null) 的两次判空;
  • 没有"忘记加锁导致重复构造"的隐患;
  • 语义上等价于 private static final AppConfig INSTANCE = ...,但初始化延后到了真正被需要的时候。

集合场景也顺手。下面这个例子用 LazyConstant 组合出一个"按需计算、但只算一次"的列表(JDK 26 也在 List / Map 接口上提供了对应的懒加载便捷工厂,思路一致):

import java.lang.LazyConstant;
import java.util.List;
import java.util.stream.IntStream;

public class ExpensiveTable {

    private static final int N = 1_000;

    // 每个元素都是延迟常量:用到才计算,且只算一次
    private static final List<LazyConstant<Double>> VALUES =
        IntStream.range(0, N)
                 .mapToObj(i -> LazyConstant.of(() -> heavyCompute(i)))
                 .toList();

    private static double heavyCompute(int i) {
        // 昂贵计算,例如查表 / 数值积分
        return Math.pow(i, 2) * Math.PI;
    }

    public static double get(int i) {
        return VALUES.get(i).get();
    }
}

性能优化视角

Lazy Constants 的核心收益是把"启动期的一次性重活"摊薄到"运行期的按需计算",直接砍掉冷启动尾延迟。更妙的是,由于 JVM 把 LazyConstant 当 final 处理,热点路径上 INSTANCE.get() 的值能被常量折叠/内联,几乎零开销。对于 Serverless、CLI、短生命周期容器这类"启动即峰值"的场景,这是免费的性能午餐。


二、原始类型模式匹配(JEP 530):switch 终于吃所有基本类型

核心概念

模式匹配(pattern matching)从 JDK 14 的 instanceof、JDK 16 的 record 模式一路走到今天,但一直有个尴尬限制:switch 的表达式/语句只认引用类型的类型模式(如 case Integer icase String scase Point(int x, int y)),原始类型(int/long/double…)只能老老实实写 case 0: case 1: 这种"裸常量",匹配到的值还拿不到。

JEP 530(第四次预览)把这个口子彻底撕开:switch 现在可以匹配任意原始类型,并且能把匹配到的值绑定到一个同类型变量上。

代码实战

把"未知状态"从无信息量的 default 变成有数据的 case int i

// 旧写法:default 里拿不到原始状态值,只能再调一次方法
String oldDescribe(int status) {
    switch (status) {
        case 0 -> { return "ok"; }
        case 1 -> { return "warning"; }
        case 2 -> { return "error"; }
        default -> { return "unknown: " + status; }
    }
}

// JEP 530:default 升级为原始类型模式,直接暴露值
String describe(int status) {
    return switch (status) {
        case 0 -> "ok";
        case 1 -> "warning";
        case 2 -> "error";
        case int i -> "unknown status: " + i;   // i 就是匹配到的 int
    };
}

配合 when 守卫做区间判断,再也不用 if-else 收尾:

String membershipTier(int yearlyFlights) {
    return switch (yearlyFlights) {
        case 0 -> "none";
        case 1, 2 -> "silver";
        case int i when i >= 100 -> "gold";
        case int i -> "regular(" + i + ")";
    };
}

instanceof 也能直接解原始类型(以前 instanceof 对原始类型毫无意义,因为 (int) 不能当引用判型;现在的语义是"安全转换式的解构"):

void handle(Object payload) {
    if (payload instanceof int i) {
        System.out.println("got primitive int: " + i);
    } else if (payload instanceof String s) {
        System.out.println("got string: " + s);
    }
}

Record 模式对原始组件也更友好了——以前组件是原始类型时,record 模式必须精确写死类型,不一致就报错;现在自动转换更贴合语言其他部分的习惯:

record Point(int x, int y) {}

void print(Object o) {
    if (o instanceof Point(int x, int y)) {   // 原始组件直接解构
        System.out.println("point at " + x + "," + y);
    }
}

架构分析:dominance 检查更严了

JEP 530 第四次预览相比前几次有两点实质变化:unconditional exactness 的定义被细化,以及 switch 的 dominance(支配)检查更严格——编译器现在能帮你揪出更多"写了也不可能命中"的分支。代价是:少数以前能编过的 switch 构造现在会被拒。升级时若遇到相关编译错误,基本是把过于宽泛的 case 顺序调整一下即可,属于"编译器替你排雷"。


三、HTTP/3 客户端(JEP 517):一行 opt-in,QUIC 红利到手

背景:为什么是 HTTP/3

HTTP/2 解决了应用层的队头阻塞(head-of-line blocking),但它跑在 TCP 上——一旦某个 TCP 包丢了,同连接上所有流都得等它重传,这就是传输层的队头阻塞。HTTP/3 把传输层换成 QUIC(基于 UDP,内置 TLS 1.3),多路复用独立成流,丢包只影响那条流;握手更快、弱网更稳。截至 JDK 26 发布,约三分之一的网站已部署 HTTP/3。

JDK 11 的 java.net.http.HttpClient 只支持 HTTP/1.1 和 HTTP/2。JEP 517 把 HTTP/3 也加上了。

代码实战:注意是 opt-in

关键点——默认协议版本不会从 HTTP/2 切到 HTTP/3,你需要显式开启。这是有意为之:避免默认行为突变导致意外。

import java.net.http.*;
import java.net.URI;

public class H3Client {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder()
            .version(HttpClient.Version.HTTP_3)   // 显式 opt-in
            .build();

        HttpRequest req = HttpRequest.newBuilder(URI.create("https://example.com"))
            .GET()
            .build();

        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println("status=" + res.statusCode() + ", version=" + res.version());
    }
}

几个落地细节:

  • 如果目标服务器不支持 HTTP/3,客户端会像以前一样透明降级到 HTTP/2(再不行到 HTTP/1.1),代码无需改动。你可以 res.version() 确认实际协商到的版本。
  • JEP 517 只做客户端,不提供服务端实现,也不暴露底层 QUIC API,也不动老 java.net.URL(它仍只有 HTTP/1.1)。
  • 想强制只走 HTTP/3、不允许降级,可以用 HttpClient.newBuilder().version(HTTP_3) 配合更细的 version 策略(如 HTTP_3 单独设置而不带 HTTP_2 兜底),但要权衡兼容性。

什么时候值得上

移动端、跨洲链路、高丢包弱网、以及大量短连接场景,HTTP/3 的收益最明显(握手快、无传输层队头阻塞)。机房内低延迟稳定网络里,HTTP/2 已经够好,不必强行切。


四、结构化并发(JEP 525):第六次预览,scope 进一步收敛

核心概念

并发编程最常见的反模式是"把一堆无关任务丢进线程池,再用 Future.get() 把异常吞掉、取消信号传不回去"。Loom 项目的结构化并发用一个 StructuredTaskScope 把这些"同属一个任务"的子任务绑成一个单元:要么一起成功,要么一个失败就取消其余,错误传播和取消语义都干净。

本次预览的变化

JDK 25 把 StructuredTaskScope 的公有构造器换成了静态工厂 open(...);JDK 26(第六次预览,JEP 525)进一步收敛:

  • 新增 Joiner.onTimeout(Duration, Supplier),超时可直接返回一个兜底结果;
  • Joiner.allSuccessfulOrThrow() 现在返回 List<结果> 而不是 Stream<Subtask>(用起来更直观);
  • Joiner.anySuccessfulResultOrThrow() 改名为 anySuccessfulOrThrow()
  • open(...) 接收 Joiner + 一个 UnaryOperator 配置函数。

代码实战:并行取数 + 超时兜底

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
import java.time.Duration;

public class ProfileService {

    record Profile(User user, java.util.List<Order> orders) {}

    public Profile load(String userId) throws Exception {
        // allSuccessfulOrThrow:任一子任务失败则整体抛异常
        try (var scope = StructuredTaskScope.open(Joiner.allSuccessfulOrThrow())) {
            Subtask<User> user  = scope.fork(() -> fetchUser(userId));
            Subtask<java.util.List<Order>> orders = scope.fork(() -> fetchOrders(userId));
            scope.join();                       // 等所有子任务结束
            return new Profile(user.get(), orders.get());
        }   // 离开 try-with-resources 时,未完成的子任务会被取消
    }

    // 带超时兜底:800ms 内没全部完成,返回降级结果而非抛异常
    public Profile loadWithTimeout(String userId) throws Exception {
        try (var scope = StructuredTaskScope.open(
                Joiner.allSuccessfulOrThrow()
                      .onTimeout(Duration.ofMillis(800), () -> FALLBACK))) {
            Subtask<User> user = scope.fork(() -> fetchUser(userId));
            Subtask<java.util.List<Order>> orders = scope.fork(() -> fetchOrders(userId));
            scope.join();
            return new Profile(user.get(), orders.get());
        }
    }

    private static final Profile FALLBACK = new Profile(User.GUEST, java.util.List.of());

    // 下面是实现占位
    private User fetchUser(String id) { return User.GUEST; }
    private java.util.List<Order> fetchOrders(String id) { return java.util.List.of(); }
}

结构化并发的精髓在最后一个右花括号:scope 关闭即取消所有还在跑的子任务,不会有"父任务结束了、子线程还在偷偷跑"的孤儿线程。配合虚拟线程(Loom 已在 JDK 21 转正),这是 Java 写高并发 IO 的最清爽范式。


五、PEM 编解码(JEP 524):终于不用引 Bouncy Castle 了

背景

密钥、证书、CRL 在网络上传输几乎都用 PEM(Privacy-Enhanced Mail)文本格式——就是那一堆 -----BEGIN PRIVATE KEY----- 包裹的 Base64。可 Java 标准库一直没有一个干净的 API 在 "PEM 文本 ↔ 密钥/证书对象" 之间互转,大家要么手写 Base64 + 解析,要么拉 Bouncy Castle。JEP 470(JDK 25)首次预览,JEP 524(JDK 26)第二次预览,把这套 API 打磨得更顺手。

本次变化

  • PEMRecord 改名为 PEM,并新增 decode() 方法返回解码后的 DER 内容;
  • EncryptedPrivateKeyInfo 的加密方法改名为 encrypt,改为接受 DEREncodable(可加密 KeyPairPKCS8EncodedKeySpec);
  • PEMEncoder / PEMDecoder 现在支持 KeyPairPKCS8EncodedKeySpec 的加解密。

代码实战

import java.security.PEM;
import java.security.PEMDecoder;
import java.security.PEMEncoder;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;

public class PemDemo {

    public static void main(String[] args) throws Exception {
        String privateKeyPem = """
            -----BEGIN PRIVATE KEY-----
            MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA...
            -----END PRIVATE KEY-----
            """;

        // PEM 文本 -> 密钥对象
        PrivateKey key = PEMDecoder.of().decodePrivateKey(privateKeyPem);

        // 证书同理
        // X509Certificate cert = PEMDecoder.of().decodeCertificate(certPem);

        // 对象 -> PEM 文本
        String out = PEMEncoder.of().encode(key);
        System.out.println(out);

        // 想拿回原始 DER 字节
        PEM pem = PEMDecoder.of().decode(privateKeyPem);
        byte[] der = pem.decode();
    }
}

注意 JEP 524 仍是第二次预览,生产使用前请确认你的 JDK 26 构建开启了预览,并评估它何时转正(PEM 这类"标准格式互转"需求极强,转正概率高)。


六、AOT 对象缓存支持任意 GC(JEP 516):Leyden 终于打通 ZGC

背景:两类尾延迟,只能二选一?

HotSpot 的 GC 大多会暂停应用线程来回收内存,于是即便 99% 请求 10ms 内返回,1% 可能飙到 100ms+(尾延迟)。ZGC 用并发回收把暂停控制在 1ms 以内,是压尾延迟的利器。

但另一类尾延迟来自冷启动:Java 常用"拉新 JVM 实例"来横向扩容,而新实例要重新加载/链接上万类、重新预热 JIT,前几个请求明显更慢。Project Leyden 的 AOT 缓存(JDK 24 的 AOT 类加载/链接、JDK 25 的 AOT 方法画像)正是为此而生——训练运行把类缓存下来,生产直接"类已就位"。Spring PetClinic 实测启动快了 41%。

矛盾来了:旧版 AOT 缓存的对象格式和具体 GC 绑定,跟 ZGC 不兼容。于是你只能在"用 ZGC 压 GC 尾延迟"和"用 AOT 缓存压启动尾延迟"之间二选一。

JEP 516 的解法

JEP 516 让 AOT 缓存以GC 中立的序列化格式存储,加载时再顺序实例化到内存,从而支持任意 GC,包括 ZGC。两类尾延迟优化现在能同时吃。

代码/命令实战

# 1) 训练运行:跑一遍典型路径,记录类与对象到缓存文件
java -XX:AOTCacheOutput=app.aotcache -cp app.jar com.example.Main

# 2) 生产运行:复用缓存,并同时开启 ZGC
java -XX:+UseZGC -XX:AOTCache=app.aotcache -cp app.jar com.example.Main

这是运维层优化,不改业务代码。对 Serverless / 容器扩缩容 / 微服务冷启动敏感的场景,组合拳收益显著:启动更快 + 暂停更短。


七、其余几个值得记账的 JEP

  • JEP 522 — G1 GC 通过减少同步提升吞吐:纯 JVM 内部优化,无 API 变化。G1 仍是默认 GC,这次主要在内部降低锁竞争,高并发分配压力下吞吐更好。升级即白拿,无需改代码。

  • JEP 529 — Vector API(第十一次孵化):用 jdk.incubator.vector 把 SIMD 指令暴露给 Java,写数值计算(矩阵、编解码、哈希)能直接吃满 CPU 向量单元。仍是孵化态,需要 --add-modules jdk.incubator.vector

    import jdk.incubator.vector.*;
    
    static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
    
    void saxpy(int[] y, int[] x, int a) {
        for (int i = 0; i < SPECIES.loopBound(y.length); i += SPECIES.length()) {
            IntVector vx = IntVector.fromArray(SPECIES, x, i);
            IntVector vy = IntVector.fromArray(SPECIES, y, i);
            vy.mul(a).add(vx).intoArray(y, i);   // 批量计算
        }
    }
    
  • JEP 500 — Prepare to Make Final Mean Final:为 Valhalla 项目铺路的"准备性" JEP,让 class 文件里的 final 在 JVM 层面真正不可被绕过地生效。对应用层基本无感知,属于"未来价值"的一环。

  • JEP 504 — Remove the Applet APIjava.applet 包正式移除(之前已弃用多年)。还在维护古董 Applet 代码的,该迁移到 javax.swing / JavaFX / 纯 Web 了。


性能优化小结(落到生产)

把本文能直接带来性能收益的点收一下口:

  1. 冷启动:Lazy Constants(按需初始化,JVM 当 final 优化)+ AOT 缓存(JDK 24/25 已铺垫,JDK 26 打通任意 GC)+ ZGC = 启动尾延迟与 GC 尾延迟双压。容器/Serverless 优先评估。
  2. 并发 IO:结构化并发 + 虚拟线程,是 Java 写"并行聚合多个下游"最干净、最不易漏取消的范式。
  3. CPU 密集计算:Vector API(孵化)适合编解码、数值内核;G1 吞吐提升属于无感白拿。
  4. 网络:跨弱网/移动端链路,HTTP/3 客户端 opt-in 一行切换,自动降级保兼容。

总结与展望:哪些该上,哪些再等等

JDK 26 的 10 个 JEP 勾勒出两条清晰主线:Amber 让代码更短更安全(模式匹配、Lazy Constants),Leyden/Panama 让 Java 更快更稳(AOT 缓存、Vector API)。对一线开发的建议:

  • 立刻能用(标准特性,无需预览):HTTP/3 客户端(JEP 517)、G1 吞吐(JEP 522)、AOT 缓存 + 任意 GC(JEP 516,运维层)、移除 Applet(JEP 504,清理)。
  • 强烈建议试,但盯紧转正节奏(预览/孵化):Lazy Constants(JEP 526,第二次预览)、原始类型模式匹配(JEP 530,第四次预览)、结构化并发(JEP 525,第六次预览)、PEM(JEP 524,第二次预览)、Vector API(JEP 529,第十一次孵化)。这些在 JDK 26 里已经相当成熟,但 API 仍可能微调——内部工具、新项目可放心吃,对外强约束的库谨慎。
  • 升级 checklist:① 用 jdeprscan / japicmp 扫一遍依赖对 Applet 等移除项的依赖;② 预览特性统一在构建脚本加 --enable-preview --source 26;③ AOT 缓存按"训练运行 → 生产加载"两段式接入 CI;④ 在弱网环境灰度验证 HTTP/3 协商与降级;⑤ 用 jmh 对核心路径做升级前后基准对比,别凭感觉判断快慢。

Java 没有停下。六个月后 JDK 27 又会带来新一轮预览与转正——而 JDK 26,正是把过去两年 Amber / Leyden 的成果"临门一脚"钉进标准的地方。现在上手,刚好卡在红利释放的窗口期。

参考(官方 JEP)

  • JDK 26 总览:https://openjdk.org/projects/jdk/26/
  • JEP 526 Lazy Constants:https://openjdk.org/jeps/526
  • JEP 530 Primitive Types in Patterns:https://openjdk.org/jeps/530
  • JEP 517 HTTP/3 for HTTP Client:https://openjdk.org/jeps/517
  • JEP 525 Structured Concurrency:https://openjdk.org/jeps/525
  • JEP 524 PEM Encodings:https://openjdk.org/jeps/524
  • JEP 516 AOT Object Caching with Any GC:https://openjdk.org/jeps/516

推荐文章

Elasticsearch 监控和警报
2024-11-19 10:02:29 +0800 CST
Vue3中如何处理跨域请求?
2024-11-19 08:43:14 +0800 CST
php常用的正则表达式
2024-11-19 03:48:35 +0800 CST
16.6k+ 开源精准 IP 地址库
2024-11-17 23:14:40 +0800 CST
程序员茄子在线接单