带有 static 和 final 限定符的奇怪 Java 行为

Strange Java behaviour with static and final qualifiers

在我们的团队中,我们发现了一些同时使用 staticfinal 限定符的奇怪行为。这是我们的测试 class:

public class Test {

    public static final Test me = new Test();
    public static final Integer I = 4;
    public static final String S = "abc";

    public Test() {
        System.out.println(I);
        System.out.println(S);
    }

    public static Test getInstance() { return me; }

    public static void main(String[] args) {
        Test.getInstance();
    }
} 

当我们运行 main 方法时,我们得到的结果是:

null
abc

两次都写null值我就明白了,因为静态class成员的代码是从上到下执行的。

谁能解释为什么会发生这种行为?

S 是一个编译时常量,遵循 JLS 15.28 的规则。因此,代码中出现的任何 S 都将替换为编译时已知的值。

如果您将 I 的类型更改为 int,您也会看到相同的结果。

这些是当您运行您的程序时采取的步骤:

  1. main 可以是 运行 之前,Test class 必须按出现顺序由 运行ning 静态初始化器初始化。
  2. 要初始化me字段,开始执行new Test()
  3. 打印I的值。由于字段类型是 Integer,看起来像编译时常量 4 的东西变成了计算值 (Integer.valueOf(4))。该字段的初始化器尚未运行,打印初始值null
  4. 打印S的值。因为它是用编译时常量初始化的,所以这个值被烘焙到引用站点,打印 abc.
  5. new Test() 完成,现在执行 I 的初始化程序。

教训:如果您依赖急切初始化的静态单例,请将单例声明放在最后一个静态字段声明中,或者求助于发生在所有其他静态声明之后的静态初始化程序块。这将使 class 看起来完全初始化为单例的构造代码。

由于 Integer 数据类型,您的行为很奇怪。关于JLS 12.4.2静态字段按照你写的顺序初始化,但是编译时常量首先被初始化。

如果您不使用包装器类型 Integer,而是使用 int 类型,您将获得所需的行为。

你的Test编译成:

public class Test {

    public static final Test me;
    public static final Integer I;
    public static final String S = "abc";

    static {
        me = new Test();
        I = Integer.valueOf(4);
    }

    public Test() {
        System.out.println(I);
        System.out.println("abc");
    }

    public static Test getInstance() { return me; }

    public static void main(String[] args) {
        Test.getInstance();
    }
}

如您所见,Test 的构造函数在 I 初始化之前被调用。这就是它为 I 打印 "null" 的原因。如果您要交换 meI 的声明顺序,您将得到预期的结果,因为 I 将在调用构造函数之前被初始化。您还可以将 I 的类型从 Integer 更改为 int

因为 4 需要自动装箱(即包装在 Integer 对象中),它不是编译时常量,而是静态初始化程序块的一部分。但是,如果类型为 int,则数字 4 将是一个编译时常量,因此不需要显式初始化。因为 "abc" 是编译时常量,所以 S 的值按​​预期打印。

如果你要替换,

public static final String S = "abc";

public static final String S = new String("abc");

然后你会注意到 S 的输出也是 "null"。为什么会这样?出于同样的原因 I 也输出 "null"。像这样的具有文字常量值的字段(不需要 需要自动装箱,如 String)在编译时带有 "ConstantValue" 属性,这意味着它们只需查看 class' 常量池即可解析值,无需 运行 任何代码。