Java基础、中级、高级、架构面试资料

JDK 27 又把 API 砍了俩方法,JEP 531 和 Set.ofLazy() 来了

JAVA herman 16浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog2,发送下载链接帮助你免费下载!
本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领
【腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云

大约 1 个月之前,我写了一篇关于 JDK 27 的文章,当时的 JDK 27 还只有一个 JEP。现在,随着时间的推进,又加入了一些 JEP。但是这些 JEP 呢,不出意外都是经过多轮预览的 JEP。

编号虽然是新的,但它们本身改进或改动并不大。

以惰性常量为例,我今天看到 JDK 27 已经加入了 JEP 531,这不就是惰性常量的第三次预览,什么时候能真正 release 呢?而且为什么又改了?

我大致看了一眼,https://openjdk.org/jeps/531,总结出了一句。那就是,JEP 531 是 JDK 27 中惰性常量的第三次预览,它从 JDK 26 的 JEP 526 “换号”而来,核心变化是做减法,移除 isInitialized()orElse() 两个方法;同时又做加法,新增 Set.ofLazy() 工厂方法,补全了三大基础集合类型的惰性支持。

本文,就展开说一说,这次预览以及这这其中变更的解析。

为什么从 526 变成了 531?

很多开发者看到 JEP 531 的文档时会注意到一个细节:它明确写着This API first previewed in JDK 25 via JEP 502... re-previewed in JDK 26 via JEP 526... we here propose to revise and re-preview the API in JDK 27

也就是说,同一个特性,三次预览,三个不同的 JEP 编号

这其实是 OpenJDK 的标准操作流程。JEP(Java Enhancement Proposal)本质上是一份“设计提案文档”,每次对 API 进行实质性修订并重新进入预览阶段时,通常都会分配一个新的 JEP 编号。JDK 25 的 JEP 502 首次预览时它还叫 StableValue;JDK 26 的 JEP 526 进行了大规模重构,改名 LazyConstant、移除低层方法、将工厂方法移入 List/Map 接口、禁止 null 值;到了 JDK 27,再次修订后又有了新的编号 JEP 531。

所以,JEP 编号的变更不代表特性被废弃,而是代表这个特性在持续迭代中。所以,我们可以把它理解为“同一个特性的 2.0、3.0 版本文档”,或者是同一个特性的多次迭代的版本号。

第三次预览的核心变化

简单来说,就是做减法又做加法。

文章配图参见 https://mp.weixin.qq.com/s/tXjFe9Oce2dDS59_TAsuLg

移除 isInitialized、orElse

在 JDK 26 的第二次预览中,LazyConstant 接口提供了三个方法。

  • get():获取值,若未初始化则触发计算
  • isInitialized():查询是否已初始化(不触发计算)
  • orElse(T other):若已初始化返回值,否则返回默认值(不触发计算)

到了 JDK 27 的 JEP 531,后两个方法被彻底移除

官方给出的理由是:as these could be used in ways not consistent with the design goals of the API。翻译过来就是,这些方法的使用方式与 API 的设计目标不一致。

这是什么意思?

惰性常量的核心设计哲学是“延迟不变性”(Deferred Immutability):一个值在首次访问前是未定义的,一旦初始化就永远不变,且初始化过程对用户应该是透明的。get() 方法完美体现了这一点,我们不需要关心它是否已经初始化,调用它就能得到值。

isInitialized()orElse()打破了这种透明性

  • isInitialized() 让你可以“窥视”内部状态,这可能导致代码中出现分支逻辑:if (lc.isInitialized()) { ... } else { ... }。这种写法实际上是在“手动管理初始化状态”,与惰性常量“自动、透明、至多一次”的设计初衷相违背。
  • orElse() 提供了“如果还没初始化就用别的值”的语义,这本质上是在鼓励“非确定性的编程模式”,调用者需要为一个可能尚未初始化的常量准备备选方案,这会让代码逻辑变得复杂且难以推理。

社区中也有开发者讨论认为,这两个方法容易让人把 LazyConstant 当成“带缓存的 Optional”来用,而它的真正定位应该是“可以延迟初始化的 final 字段”。移除这两个方法,就是在强制开发者回归简单、纯粹的常量语义

新增 Set.ofLazy()

JDK 26 的第二次预览引入了 List.ofLazy()Map.ofLazy(),让集合中的每个元素都可以独立惰性初始化,但 Set 类型当时缺席了。

JEP 531 新增了 Set.ofLazy(),至此 Java 三大基础集合类型(ListSetMap)都有了对应的惰性工厂方法。

官方给出的典型用例是配置选项的惰性判定

class Application {
    enum Option { VERBOSE, DRY_RUN, STRICT }

    // 每个选项是否启用,首次查询时才执行昂贵的判定逻辑
    static final Set<Option> OPTIONS =
        Set.ofLazy(EnumSet.allOf(Option.class), Application::isEnabled);

    private static boolean isEnabled(Option option) {
        // 解析命令行、读取配置文件、查询数据库...
        return ...;
    }

    public static void process() {
        if (OPTIONS.contains(Option.DRY_RUN)) {
            return; // 只有 DRY_RUN 被查询时,才会触发 isEnabled(DRY_RUN)
        }
        // ...
    }
}

这个例子非常精妙:Set.ofLazy() 接收两个参数,一个预定义的元素候选集(这里是所有枚举值),以及一个判定函数。集合中的每个元素的成员资格都存储在一个独立的 LazyConstant 中,首次 contains() 查询时才计算该元素是否属于集合。

这也意味着:

  • 如果我们只查询 DRY_RUN,其他选项的判定逻辑永远不会执行
  • 每个选项的判定结果至多计算一次
  • 初始化后的结果可以被 JVM 常量折叠优化

API 的三年进化史

从“稳定值”到“惰性常量”,包含了这个 API 的三年进化史。

回顾这个特性的演进,我们能清晰看到 OpenJDK 团队从低层机制到高层抽象的设计思路转变。

阶段JDK 版本JEP 编号名称核心变化
第一次预览JDK 25JEP 502Stable Values低层 API,提供 orElseSetsetOrThrowtrySet 等底层控制方法
第二次预览JDK 26JEP 526Lazy Constants大重构:改名、移除底层方法、工厂方法移入集合接口、禁止 null
第三次预览JDK 27JEP 531Lazy Constants精修:移除 isInitialized()/orElse(),新增 Set.ofLazy()

三次迭代,一次比一次简洁,一次比一次“聚焦”。

JDK 25 的 StableValue 更像是一个“并发原语工具箱”;到了 JDK 26,它变成了“惰性常量容器”;JDK 27 则进一步提纯,只保留最符合“常量”本质的语义。

这种做减法的设计哲学在 Java 近年的新特性中越来越常见。Project Amber 的模式匹配、Project Loom 的结构化并发,都是在反复预览中不断砍掉“看起来有用但会引入复杂性”的边缘功能。

为什么需要 ofLazy() 工厂方法

可能有读者会问:既然已经有了 LazyConstant.of(),为什么还要在 ListSetMap 接口上新增 ofLazy()

答案是组合性与性能

考虑一个对象池的场景。用 LazyConstant 单个实现。

// 笨拙:需要手动管理数组和索引
static final LazyConstant<OrderController>[] ORDERS = ...;

而用 List.ofLazy()

static final List<OrderController> ORDERS =
    List.ofLazy(POOL_SIZE, _ -> new OrderController());

后者不仅代码更简洁,更重要的是:JVM 可以对集合中的每个惰性元素独立进行常量折叠List.ofLazy() 返回的列表中,每个元素都存储在 @Stable 注解的字段中,JVM 信任这些值初始化后不再变化,从而可以进行激进的内联和缓存优化。

Set.ofLazy() 的加入则补全了最后一个拼图。当我们需要“一组预定义候选值的惰性判定”时,不再需要手动用 Map.ofLazy() 模拟集合语义。

对开发者的实际意义

这个功能可能还不是最后一次预览,按照官方现在的尿性,我估计说不定到 JDK 29 才能真正的“落地”。

虽然,但是,这个功能对我们却有这非常多的实际意义。

启动性能

惰性常量最直接的好处是“改善应用启动时间”。大型应用中,组件的初始化往往构成启动瓶颈。用 LazyConstant 重构后,组件只在真正被使用时才初始化:

class Application {
    static final LazyConstant<OrderController> ORDERS = LazyConstant.of(OrderController::new);
    static final LazyConstant<UserService> USERS = LazyConstant.of(UserService::new);

    public static OrderController orders() { return ORDERS.get(); }
}

线程安全

LazyConstant 内部通过 @Stable 注解和内存屏障保证初始化至多发生一次,且无需显式同步。这比手写双重检查锁定(DCL)或 volatile 字段更安全、更高效。

JVM 优化

只要将 LazyConstant 存储在 final 字段中,JVM 就能对其内容进行常量折叠。这意味着:

Application.orders().getLogger().info("...");

如果 orders()getLogger() 都返回存储在 final 字段中的惰性常量,JVM 可能在编译时直接内联最终的 Logger 实例,消除方法调用开销。

总结与展望

JEP 531 的第三次预览显示,惰性常量特性正在走向成熟。经过三次迭代,API 表面已经相当精简了。

  • 核心接口 LazyConstant 只剩下 get() 和工厂方法
  • 三大集合类型的 ofLazy() 工厂方法补齐
  • 禁止 null,避免语义模糊
  • 移除所有可能破坏”透明惰性”的方法

按照 OpenJDK 的惯例,一个预览特性通常经历 2-4 轮预览后就会最终确定。JEP 531 已经是第三轮,且本轮改动幅度明显小于上一轮(JDK 26 的重构幅度远大于 JDK 27 的精修)。我们很可能最早在 JDK 28 看到它成为正式特性。

对于 Java 开发者来说,这意味着不久后我们就能用一种声明式、线程安全、JVM 可优化的方式来替代手写的 DCL 和 volatile 懒加载模式,而且代码会更简洁、更可靠。

期待更多的 JEP 稳定落地,让 Java 再次伟大!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!

本文原文出处:业余草: » JDK 27 又把 API 砍了俩方法,JEP 531 和 Set.ofLazy() 来了