JEP 531:惰性常量(第三次预览)
引言
在性能和优雅之间做取舍,是每个 Java 开发者都熟悉的日常。而在"延迟初始化"这个问题上,这种取舍尤为刺眼:你要么选择快、但必须提前初始化的 static final,要么选择灵活、但笨重且易错的懒加载写法。JEP 531 提出的惰性常量,正是要终结这个二选一的困境。
延迟初始化的老问题
Java 开发者长期面临一个两难。
一方面,static final 字段性能极好。因为一旦被赋值就不会再变,JVM 可以把它当作真正的常量来做激进优化——常量折叠、内联、去除冗余读取等等。这是 Java 性能模型里最锋利的一把刀。
但它有一个硬性要求:**必须在类加载时就急切(eagerly)完成初始化。**类一被加载,字段就得算出值来。
问题就在这里:**如果初始化的代价很高怎么办?**比如要编译一个复杂的正则、要读一个大文件、要建立一个昂贵的连接。你并不希望在类加载的那一刻就付出这个代价——你希望它推迟到真正第一次用到的时候。
于是你被迫求助于各种延迟初始化的惯用写法:
- Holder 类惯用法(holder idiom):为每个惰性值单独写一个私有静态嵌套类,利用 JVM"类首次使用时才初始化"的语义来实现延迟。
- 双重检查锁(double-checked locking):配合
volatile,小心翼翼地手写线程安全的懒加载。 Supplier包装:把值藏在一个函数背后,首次调用时计算。
这些写法有两个共同的毛病:它们容易写错(双重检查锁的正确写法出了名地微妙),而且它们挡住了你真正想要的常量级优化——因为对 JVM 来说,一个普通字段或一个 Supplier 背后的值,随时可能变,无法当常量对待。
惰性常量是什么
惰性常量的核心想法其实非常简单:一个最多被赋值一次、赋值之后就永不改变的持有者。
- 它在创建时是空的。
- 它的值在首次访问时才计算。
- 整个初始化过程是线程安全的,而且天然避开了双重检查锁的种种坑。
- 最关键的是:JVM 知道这个值一旦被设定就不会再变,因此可以把已初始化的内容视为可信,对它施加与
final字段同等级别的激进优化。
换句话说,惰性常量把"延迟"和"常量优化"这两个原本互斥的属性,第一次装进了同一个概念里。
用法:LazyConstant API
JEP 531 提供了预览 API LazyConstant。通过 LazyConstant.of(...) 创建它,并让初始化逻辑紧邻字段声明:
class Config {
// computed on first access, then constant
private static final LazyConstant<Pattern> PATTERN =
LazyConstant.of(() -> Pattern.compile("complex-regex"));
// instance lazy field
private final LazyConstant<Logger> logger =
LazyConstant.of(() -> Logger.getLogger(getClass().getName()));
}
注意这里没有嵌套类,没有 volatile,没有 synchronized 块。初始化逻辑就紧挨着字段声明,一眼就能看明白这个字段是什么、怎么算出来的。线程安全和优化的所有细节,都由编译器和 JVM 替你处理。
前后对比
此前——holder 惯用法:
- 每个惰性值都要配一个私有静态嵌套类,或者手写一套
synchronized/volatile双重检查锁。 - 这些代码正确,但啰嗦,而且稍不留神就会写出微妙的错误。
- 更糟的是,它们从根本上阻断了常量级优化。
此后——惰性常量:
- 一个字段就装下这个惰性值。
- 初始化逻辑就写在声明旁边,可读性极高。
- 它拥有接近
static final的优化空间。
代码更少,出错的机会也更少。
什么场景最受益
惰性常量最适合那些初始化代价高昂、但一旦创建就不再改变的对象:
- 昂贵的单例:缓存、解析器、预编译好的正则表达式。这些东西建起来很贵,但建好之后就是只读的。
- 只读一次、之后当作不可变的配置。
- **更快的应用启动。**因为这些昂贵的初始化工作从"类初始化的关键路径"上被挪走了,只有真正用到时才付出代价,应用的冷启动时间随之缩短。
- **通向 Leyden 式提前初始化的垫脚石。**惰性常量的语义(一次赋值、之后可信)为 Project Leyden 的提前(ahead-of-time)初始化工作铺平了道路。
预览状态
需要注意的是,这仍然是一个预览特性,而且已经是第三次预览了。
- 它在 JDK 27 中作为第三次预览发布,API 仍可能根据社区反馈继续调整。
- 编译和运行时都需要加上
--enable-preview标志。 - 如果你之前关注过更早的 "stable values" 预览,这一版正是在那个基础上、根据反馈迭代改进而来的。
结论
有了惰性常量,你不用再在"快但急切"和"懒但慢"之间做痛苦的选择。首次使用时才初始化,之后拿到常量级的优化,同时拥有 LazyConstant API 和内置线程安全。