JDK27升后LazyConstant怎么用
升到 JDK 27 后,JEP 531(Lazy Constants,Third Preview)随版本交付(Status Closed/Delivered,Release=27;relnotes 亦收录)。一句话先钉死:它仍是 preview API,编译和运行都要 --enable-preview;不要把「Closed/Delivered」理解成「可当正式 API、可免开关」。
现场更常见的坑是另一类:组件里 Logger、仓库、连接工厂一类字段用 final 急初始化,启动路径被拖慢;改成可变字段又丢常量折叠,还得自己写双重检查。本篇只谈一件事:在 JDK 27 上怎么用 LazyConstant、和 final 差在哪、怎么验、preview 边界在哪。同系列结构化并发另见 《JDK27升后结构化并发preview怎么写》;JFR 脱敏、紧凑对象头各有专篇(167650309、167451590),本稿不抢那几条主线。
痛点:final 急初始化拖启动,手写懒加载又易翻车
三类常见混乱:
final必须在构造/类初始化期赋值——实例一建出来,Logger.create(...)、读配置、建连接池就全跑完了,哪怕本请求根本用不到。- 改成可变字段手写懒加载——
synchronized一把大锁,或 double-check 漏volatile;多线程下可能建多个实例,副作用(打日志、占端口、写文件)重复发生。 - 丢掉常量折叠——字段不再是
final,JIT 很难把后续读取当成「永远不变」做折叠;为了懒加载,把性能优化也一起丢了。
理想状态是:内容至多算一次、算完不可变、字段仍可走常量信任链,且首次访问才触发。java.lang.LazyConstant 就是冲着这件事来的。

边界:JEP 531 第三预览,仍须开 preview
口径先钉死(openjdk.org/jeps/531;Oracle JDK 27 lazy-constants 文档 Note;SE 27 LazyConstant javadoc):
- 状态:Closed / Delivered,Release = 27;History 写明「第三预览」(Third Preview)。
- 性质:preview API,默认关闭。
- 开关:
javac --release 27 --enable-preview …与java --enable-preview …两边都要开。注意:openjdk.org/jeps/531 页示例若仍写--release 26,那是文稿遗留——对 JDK 27 写 27,不要照抄。 - 现行类名:
java.lang.LazyConstant。JDK 26(JEP 526)曾把 API 从StableValue重命名过来;正文里历史一句带过即可,勿把 StableValue 当 27 现行 API。 - 勿混下一轮:JEP 547(Fourth Preview)目前是 Candidate,文中写拟在 JDK 28 再预览且 无 API 变更——那是后续提案,不要写成 27 已交付行为。27 交付边界以 JEP 531 + SE 27 javadoc 为准。
# 编译(两边都要带 --enable-preview)
javac --release 27 --enable-preview Component.java
# 运行
java --enable-preview Component
为什么必须开 --enable-preview?
因为官方就这么定的。 Closed/Delivered 只表示该 JEP 随 27 进了发行版;交付物本身仍是第三轮预览。javac 开了、java 没开,或反过来,都会在一边过、一边挂。不要暗示「27 起默认可免开关」。
LazyConstant 和 final 字段有什么差别?
| 维度 | final T | final LazyConstant<T> |
|---|---|---|
| 赋值时机 | 构造/类初始化必须完成 | of(Supplier) 时未初始化;首次 get() 才算 |
| 内容可变性 | 引用不可变;对象本身另说 | 初始化后内容不可再改(holder 语义) |
| 可否为 null | 可以(视业务) | 不可:计算返回 null → 错误态 |
| 并发首次访问 | 无「算几次」问题(已急初始化) | 多线程竞态时至多一次计算 |
| 常量折叠 | 经典路径 | 存于 final 字段(及文档所述信任链)时,JVM 可对已初始化内容做折叠 |
一句话:final 管的是「引用何时钉死」;LazyConstant 管的是「内容何时算、算完钉死」。把 holder 本身放在 final 字段里,两边的好处可以叠在一起。
LazyConstant.of 和 get 怎么写?
官方最小形态(SE 27 javadoc 示例口径):
import java.lang.LazyConstant;
import java.util.logging.Logger;
public class Component {
// 创建时未初始化;字段宜 final,便于常量折叠信任链
private final LazyConstant<Logger> logger =
LazyConstant.of(() -> Logger.getLogger(Component.class.getName()));
public void process() {
logger.get().info("Process started");
// ...
}
public static void main(String[] args) {
new Component().process();
}
}
要点拆开看:
LazyConstant.of(Supplier):返回未初始化的 holder;传入的计算函数此时不会执行。get():首次调用时执行计算函数,用结果初始化;之后永远返回同一内容。- 字段写成
final:文档 Performance 节写明——常量折叠依赖从static final(或经信任链:static final/ record 字段 / hidden class 的 final 实例字段)直达 lazy constant。实例字段场景也建议final,把「holder 不可替换」钉死。 - 内容不可为 null:需要可空语义时,用
Optional包一层,而不是让 Supplier 返回 null。
组合:一个 LazyConstant 依赖另一个
import java.lang.LazyConstant;
public final class Graph {
public static final class Foo {
// ...
}
public static final class Bar {
public Bar(Foo foo) {
// ...
}
}
static final LazyConstant<Foo> FOO = LazyConstant.of(Foo::new);
static final LazyConstant<Bar> BAR = LazyConstant.of(() -> new Bar(FOO.get()));
public static Foo foo() {
return FOO.get();
}
public static Bar bar() {
return BAR.get();
}
public static void main(String[] args) {
// 首次 bar():若 FOO 未就绪,会先算 FOO,再算 BAR
System.out.println(bar());
}
}
调用 BAR.get() 时,若依赖的 FOO 还没初始化,会先算 FOO。形成依赖图可以,但别搞成环:计算函数经同一 holder 递归自调用,会进错误态(见下节)。
集合侧还有 List.ofLazy / Map.ofLazy / Set.ofLazy(均为 27 preview;其中 Set.ofLazy 相对 26 是增量)——适合「按索引 / 按键 / 按候选」惰性填槽,本稿不展开成长文。
多线程同时 get 会算几次?
至多一次。 Thread Safety 节明文:竞态时只有一个线程跑计算函数(在调用方线程上执行),其余线程阻塞到初始化完成(或失败进入错误态);胜者更新后,其它线程观察已初始化(或错误态),不会再算一遍。初始化与内容发布之间有 happens-before,新对象上的 final 字段可安全发布。
注意边界:
- 中断不取消初始化:计算线程被 interrupt,
get()不会清中断状态,也不会替你抛InterruptedException。 - 无限阻塞无超时:计算函数若一直堵着,其它在
get()上等的线程也会一直堵——没有内建 timeout / cancel。

计算失败或返回 null 会怎样?
失败路径按 javadoc「Exception handling」对齐,不要凭印象写:
- 计算函数抛未检查异常(运行时异常或 Error)→ holder 不初始化,转入错误态,本次
get()抛NoSuchElementException,cause 为原异常。 - 之后再
get()→ 仍抛NoSuchElementException,不再调用计算函数;后续调用无 cause,message 里会带上原异常的类名。 - 返回
null→ 同样进错误态;首次NoSuchElementException的 cause 为NullPointerException。 - 计算函数经同一 lazy constant 递归自调用 → 首次
NoSuchElementException的 cause 为IllegalStateException。
import java.lang.LazyConstant;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;
public class ErrorStateDemo {
static final AtomicInteger attempts = new AtomicInteger();
static final LazyConstant<String> BAD = LazyConstant.of(() -> {
attempts.incrementAndGet();
throw new IllegalArgumentException("boom");
});
static final LazyConstant<String> NULLISH = LazyConstant.of(() -> {
attempts.incrementAndGet();
return null; // 禁止:会进错误态
});
public static void main(String[] args) {
try {
BAD.get();
} catch (NoSuchElementException ex) {
// 首次:cause 为 IllegalArgumentException
System.out.println("1st cause=" + ex.getCause());
}
try {
BAD.get();
} catch (NoSuchElementException ex) {
// 后续:无 cause;message 含原异常类名
System.out.println("2nd cause=" + ex.getCause()
+ ", msg=" + ex.getMessage());
}
System.out.println("attempts for BAD path observed via separate constant");
try {
NULLISH.get();
} catch (NoSuchElementException ex) {
// cause 为 NullPointerException
System.out.println("null path cause=" + ex.getCause());
}
}
}
验收时盯两件事:错误态不可恢复(不会「下次再试一次」),以及 null 与抛异常走同一类错误态语义。业务上若需要可空,用 Optional;若需要可重试,不要指望同一个 LazyConstant 实例帮你重算——换工厂、换实例,或根本别用惰性常量表达「可失败可重试」的流程。
怎么验:单线程一次、并发一次、错误态、preview 开关
建议按四步过,每步只验一件事:
-
Preview 开关
去掉--enable-preview应无法正常使用该 preview API;javac/java两侧都带上才能跑通上文Component。 -
单线程只算一次
在 Supplier 里打计数器或副作用日志;连续get()两次,计数应为 1,两次引用相同(==)。 -
两线程并发仍只算一次
两个线程同时get(),计数仍为 1;两边拿到同一实例。可对照上面的时序图。 -
故意失败 / 返回 null
看首次NoSuchElementException的 cause,再看第二次无 cause、且计数不再增加。
可选对照:同一场景改回手写 synchronized 懒加载,观察锁竞争与「忘了 volatile」时的风险——用来理解「为什么要换成 LazyConstant」,不是上线必跑项。
边界再钉几句
- Preview 不是正式 API:Closed/Delivered ≠ 可免
--enable-preview;生产是否采用,按「是否真有延后初始化收益」评估,并接受后续预览轮次仍可能改签名。 - 字段宜
final:常量折叠信任链写在文档里;别把 holder 放进普通可变字段还指望同等优化。 - 强引用内容:初始化后内容不会被 holder 丢掉;可达的 lazy constant 会让内容一直可达——长期缓存大对象时留意泄漏面。
- 不可序列化:
LazyConstant不是Serializable。 - 静态初始化顺序:用在静态初始化器里可能与类初始化顺序交互;环状初始化按 JLS §12.4 可能直接炸——别用它硬拧启动环。
- 同系列别文:结构化并发、JFR 脱敏、紧凑对象头各有专篇(文首内链),本文不抢那些主线。
选型时别踩的两个坑
第一,把「交付」当成「可免 preview」。Closed/Delivered 只说明 JEP 531 随 JDK 27 进了发行版,并不取消 preview 开关,也不等于下一轮(JEP 547 / 拟 28)可以无视。第二,用 LazyConstant 表达可重试流程。错误态是终态;需要重试、需要可空、需要可替换实现,换别的工具。先把 of / get、并发至多一次、失败进错误态三条跑通,再决定哪些 final 急初始化值得迁。
小结
| 项 | 口径 |
|---|---|
| 交付 | JEP 531,Third Preview,Closed/Delivered @ JDK 27 |
| 开关 | javac / java 都要 --enable-preview(--release 27,勿抄 JEP 页遗留的 26) |
| 写法 | final LazyConstant<T> x = LazyConstant.of(supplier); → x.get() |
| 并发 | 多线程 get 至多一次计算 |
| 失败 | 未检查异常 / null / 递归自调用 → 错误态 → NoSuchElementException(cause 规则见 javadoc) |
| 集合 | List/Map/Set.ofLazy 均为 27 preview;Set.ofLazy 为相对 26 增量 |
| 旧名 | StableValue 仅 26 历史;现行名 LazyConstant |
| 勿混 | JEP 547(拟 28、无 API 变更)≠ 27 已交付行为 |
先把 preview 开关和最小 of / get 跑通,再验并发与错误态;集合惰性工厂放到第二步。它依旧是 preview——写对、验清,比急着全仓替换 final 更重要。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Amax3747/article/details/168308773




