带有 static 和 final 限定符的奇怪 Java 行为
Strange Java behaviour with static and final qualifiers
在我们的团队中,我们发现了一些同时使用 static
和 final
限定符的奇怪行为。这是我们的测试 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
,您也会看到相同的结果。
这些是当您运行您的程序时采取的步骤:
- 在
main
可以是 运行 之前,Test
class 必须按出现顺序由 运行ning 静态初始化器初始化。
- 要初始化
me
字段,开始执行new Test()
。
- 打印
I
的值。由于字段类型是 Integer
,看起来像编译时常量 4
的东西变成了计算值 (Integer.valueOf(4)
)。该字段的初始化器尚未运行,打印初始值null
。
- 打印
S
的值。因为它是用编译时常量初始化的,所以这个值被烘焙到引用站点,打印 abc
.
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"
的原因。如果您要交换 me
和 I
的声明顺序,您将得到预期的结果,因为 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' 常量池即可解析值,无需 运行 任何代码。
在我们的团队中,我们发现了一些同时使用 static
和 final
限定符的奇怪行为。这是我们的测试 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
,您也会看到相同的结果。
这些是当您运行您的程序时采取的步骤:
- 在
main
可以是 运行 之前,Test
class 必须按出现顺序由 运行ning 静态初始化器初始化。 - 要初始化
me
字段,开始执行new Test()
。 - 打印
I
的值。由于字段类型是Integer
,看起来像编译时常量4
的东西变成了计算值 (Integer.valueOf(4)
)。该字段的初始化器尚未运行,打印初始值null
。 - 打印
S
的值。因为它是用编译时常量初始化的,所以这个值被烘焙到引用站点,打印abc
. 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"
的原因。如果您要交换 me
和 I
的声明顺序,您将得到预期的结果,因为 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' 常量池即可解析值,无需 运行 任何代码。