JEP 草案:计算常量ComputedConstant


计算常量是最多初始化一次的保持器对象。它保证在不晚于第一次访问时被初始化。

这是Java中定义单例 的另一种方式。

概括
引入计算常量,它们是最多初始化一次的不可变值持有者。计算常数提供了最终字段的性能和安全优势,同时在初始化时间方面提供了更大的灵活性。这是一个预览 API

目标

  • 将计算常量的初始化与其包含的类或对象的初始化分离。
  • 为计算常量及其集合提供简单直观的 API。
  • 对计算常量启用常量折叠优化。
  • 支持计算常量之间的数据流依赖性。
  • 减少要执行的静态初始化程序代码和/或字段初始化的数量。
  • <clinit>通过应用上述内容,可以理清依赖关系。
  • 即使在多线程环境中,也能保持完整性和一致性。

动机
大多数 Java 开发人员都听过“更喜欢不变性”的建议(Effective Java,第 17 条)。不可变性带来了许多优点:不可变对象只能处于一种状态,该状态由其构造函数仔细控制;不可变对象可以与不受信任的代码自由共享;不变性支持各种方式的运行时优化。

Java 管理不变性的主要工具是final字段(以及最近的record类)。

不幸的是,final字段有限制。它们必须尽早设置;

  • Final 实例字段必须在构造函数末尾设置,
  • 静态 Final 字段必须在类初始化期间设置。
  • 此外,顺序final 字段初始值设定项的执行在编译时确定,然后在生成的类文件中明确显示。

这样,final 字段的初始化在时间上是固定的;不能任意向前或向后移动。

这意味着开发人员被迫在最终性及其所有好处和初始化时间的灵活性之间做出选择。
开发人员设计了多种策略来改善这种不平衡,但都不是理想的。

预览功能
计算常量是一个预览 API,默认情况下处于禁用状态。要使用计算常量 API ,必须传入JVM 标志--enable-preview :

  • 使用 javac --release 22 --enable-preview Main.java 编译程序,然后使用 java --enable-preview Main 运行程序;
  • 使用源代码启动器时,使用 java --source 22 --enable-preview Main.java 运行程序;
  • 使用 jshell 时,用 jshell --enable-preview 启动它。


代码实现如下:

class Bar {
          // 1. 声明一个计算常量值
    private static final ComputedConstant<Logger> LOGGER =
            ComputedConstant.of( () -> Logger.getLogger("com.foo.Bar") );

    static Logger logger() {
        // 2. Access the computed value 
        //    (evaluation made before the first access)
         // 2. 访问计算值
        // (在第一次访问之前进行计算)
        return LOGGER.get();
    }
}

  • 计算常量ComputedConstant是最多初始化一次的持有者对象。它保证在第一次访问时初始化。
  • 它被表示为 类型的对象ComputedConstant,它与Future 一样,是某些可能已经发生或尚未发生的计算的持有者。
  •  ComputedConstant实例是通过提供值提供者来创建的,通常采用 lambda 表达式或方法引用的形式

looger()方法多次调用每次调用都会产生相同的值。
这在本质上与 Holder 类习惯用法类似,并提供相同的性能、常量折叠和线程安全特性,但更简单,并且由于不需要额外的类而导致静态占用空间更低。 
保证每个ComputedConstant 实例最多调用一次的值提供。

在上面的示例中,每次加载包含Bar类时,值提供者LOGGER最多被调用一次(反过来,最多可以加载到任何给定的类中一次)。
值提供者可以返回一个值,这个值将被视为绑定的值。

补充:
上面代码类似下面双重锁定方式:

// Double-checked locking idiom
class Foo {

    private volatile Logger logger;

    public Logger logger() {
        Logger v = logger;
        if (v == null) {
            synchronized (this) {
                v = logger;
                if (v == null) {
                    logger = v = Logger.getLogger("com.foo.Bar");
                }
            }
        }
        return v;
    }
}
...
foo.logger().log(...);