本博客日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 三大基础集合类型(List、Set、Map)都有了对应的惰性工厂方法。
官方给出的典型用例是配置选项的惰性判定。
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 25 | JEP 502 | Stable Values | 低层 API,提供 orElseSet、setOrThrow、trySet 等底层控制方法 |
| 第二次预览 | JDK 26 | JEP 526 | Lazy Constants | 大重构:改名、移除底层方法、工厂方法移入集合接口、禁止 null |
| 第三次预览 | JDK 27 | JEP 531 | Lazy Constants | 精修:移除 isInitialized()/orElse(),新增 Set.ofLazy() |
三次迭代,一次比一次简洁,一次比一次“聚焦”。
JDK 25 的 StableValue 更像是一个“并发原语工具箱”;到了 JDK 26,它变成了“惰性常量容器”;JDK 27 则进一步提纯,只保留最符合“常量”本质的语义。
这种做减法的设计哲学在 Java 近年的新特性中越来越常见。Project Amber 的模式匹配、Project Loom 的结构化并发,都是在反复预览中不断砍掉“看起来有用但会引入复杂性”的边缘功能。
为什么需要 ofLazy() 工厂方法
可能有读者会问:既然已经有了 LazyConstant.of(),为什么还要在 List、Set、Map 接口上新增 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邮箱。商务合作也可添加作者微信进行联系!