为什么静态字段没有及时初始化?

53
以下代码会打印一次 null
class MyClass {
   private static MyClass myClass = new MyClass();
   private static final Object obj = new Object();
   public MyClass() {
      System.out.println(obj);
   }
   public static void main(String[] args) {}
}
为什么静态对象不会在构造函数运行前初始化?
更新:
我没有认真检查,只是复制了这个示例程序,我以为我们在讨论2个对象字段,现在发现第一个是MyClass字段.. :/
5个回答

45
因为静态变量按照它们在源代码中的顺序进行初始化。
看看这个:
class MyClass {
  private static MyClass myClass = new MyClass();
  private static MyClass myClass2 = new MyClass();
  public MyClass() {
    System.out.println(myClass);
    System.out.println(myClass2);
  }
}

那将会打印:

null
null
myClassObject
null

编辑

好的,让我们画出来以更清晰一些。

  1. 静态字段按照在源代码中声明的顺序一个接一个地初始化。
  2. 由于第一个静态字段在其初始化期间先于其余静态字段,因此其余静态字段在其初始化期间为null或默认值。
  3. 在第二个静态字段初始化期间,第一个静态字段是正确的,但其余静态字段仍然为null或默认值。

清楚吗?

编辑2

如Varman所指出,它自己的引用在初始化期间将为null。如果你想想的话就会有意义。


3
由于myClass本身是静态的,所以...... - BalusC
好的,静态变量按顺序初始化。而且静态变量在构造函数之前,那么为什么构造函数运行时它没有被初始化呢?对我来说看起来像是一个bug。 - The Student
@Tom,不对。静态成员不在构造函数之前初始化。当构造函数分别被调用时,静态成员也被初始化。在我的例子中,当第一个静态成员与MyClass一起初始化时,构造函数被调用。当构造函数运行时,myClass会被初始化(因为它正在运行自己),但是myClass2没有被初始化。当第二个MyClass被初始化时,它再次调用构造函数,这时myClass已经被初始化,而此时myClass2正在被初始化。 - Pyrolistical
在构造函数运行时,静态变量是否在另一个线程中初始化?你有没有一篇详细解释这个问题的相关链接? - The Student
2
@Pyrolistical:当我执行你的程序时,得到了不同的结果。它打印出了 null null myClassObjectref null - Vaman Kulkarni
显示剩余3条评论

28

让我们试着用另一种方式来解释这个问题...

这是当你第一次引用类MyClass时JVM执行的顺序。

  1. 将字节码加载到内存中。
  2. 清除静态存储的内存(二进制零)。
  3. 初始化类:
    1. 按照出现顺序执行每个静态初始化程序,包括静态变量和static { ... }块。
    2. JVM然后将你的myClass静态变量初始化为MyClass的新实例。
    3. 当这发生时,JVM注意到MyClass已经被加载(字节码)并正在初始化过程中,因此跳过初始化。
    4. 在堆上为对象分配内存。
    5. 执行构造函数。
    6. 输出obj的值,仍然为null(因为它不是堆的一部分且构造函数初始化变量)。
    7. 当构造函数完成时,执行下一个静态初始化程序,该程序将obj设置为Object的新实例。
  4. 类初始化完成。从这一点开始,所有的构造函数调用都会像你预期的那样表现——也就是说,obj不会是null,而是一个指向Object实例的引用。

请记住,Java指定final变量只被赋值一次。这并不意味着当代码引用它时保证已经分配了一个值,除非你确保在分配后代码引用它。

这不是一个错误。这是处理类在自身初始化期间使用的定义方式。如果不是这样,那么JVM将进入一个无限循环。请参见第3.3步骤(如果JVM不跳过正在初始化过程中的类的初始化,则会不断地初始化——无限循环)。

还要注意的是,所有这些都发生在首次引用该类的同一线程上。其次,JVM保证在任何其他线程被允许使用该类之前,初始化将完成。


1
很棒的答案,Kevin,这里最好的一个。 - Newtopian
我想你的意思是它跳过了“静态”初始化。 - stdout

19

这是因为Java按照声明顺序执行静态部分。在你的情况下,顺序是:

  1. 新建MyClass
  2. 新建Object

当执行#1时,obj尚未初始化,因此打印null。尝试以下代码,你会看到不同之处:

class MyClass {
  private static final Object obj = new Object();
  private static MyClass myClass = new MyClass();
  public MyClass() {
    System.out.println(obj); // will print null once
  }
}

总的来说,最好完全避免使用这样的结构。如果你要创建一个单例模式,应该像这样编写代码片段:

class MyClass {

  private static final MyClass myClass = new MyClass();

  private Object obj = new Object();

  private MyClass() {
    System.out.println(obj); // will print null once
  }
}

关于<clinit>执行顺序的正确性。然而,由于构造函数是公共的,因此Singleton并不真正是一个Singleton。 - Rob Heiser
好的,静态变量按照顺序进行初始化。静态变量在构造函数之前被声明和要求进行初始化,所以为什么在构造函数运行时它还没有被初始化呢?对我来说真的看起来像一个错误。 - The Student
@Tom,你必须明白在静态方法中调用new MyClass()就是在调用构造函数。 - Pyrolistical
我觉得我们在这里出现了沟通问题。我知道new MyClass()是对构造函数的调用,但这并不能解释为什么静态字段在构造函数运行时为空。就像实例字段在构造函数之前被初始化一样,静态字段也应该是这样的...但为什么它们不是呢? - The Student
3
静态成员在实例构造函数运行之前就已经被构建了。只是在你的代码中,你的静态初始化器还调用了你的构造函数。这是一个鸡生蛋的问题。 - Dean Harding

0

这是因为静态字段按照定义的顺序进行初始化。


这并没有回答为什么在构造函数时它是空的。 - The Student

0

@Pyrolistical

由于myclass的第一个静态字段尚未完全构造,因此我得到的结果是

null null testInitialize.MyObject@70f9f9d8 null


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接