Java中静态/实例初始化块的运行顺序是什么?

106

一个项目包含多个类,每个类都有一个静态初始化块。这些块以什么顺序运行?我知道在一个类内部,这样的块按照它们在代码中出现的顺序运行。我读过一些资料说在不同的类中也是如此,但我写的一些示例代码与此不符。我用了以下代码:

package pkg;

public class LoadTest {
    public static void main(String[] args) {
        System.out.println("START");
        new Child();
        System.out.println("END");
    }
}

class Parent extends Grandparent {
    // Instance init block
    {
        System.out.println("instance - parent");
    }

    // Constructor
    public Parent() {
        System.out.println("constructor - parent");
    }

    // Static init block
    static {
        System.out.println("static - parent");
    }
}

class Grandparent {
    // Static init block
    static {
        System.out.println("static - grandparent");
    }

    // Instance init block
    {
        System.out.println("instance - grandparent");
    }

    // Constructor
    public Grandparent() {
        System.out.println("constructor - grandparent");
    }
}

class Child extends Parent {
    // Constructor
    public Child() {
        System.out.println("constructor - child");
    }

    // Static init block
    static {
        System.out.println("static - child");
    }

    // Instance init block
    {
        System.out.println("instance - child");
    }
}

运行结果如下:

开始
静态 - 祖父类
静态 - 父类
静态 - 子类
实例 - 祖父类
构造函数 - 祖父类
实例 - 父类
构造函数 - 父类
实例 - 子类
构造函数 - 子类
结束

从中可以看出,父类的块在子类的块之前运行,但这可能只是巧合,并且对于不在同一继承层次结构中的两个类没有帮助。

编辑:

我修改了我的示例代码,将以下内容追加到LoadTest.java文件中:

class IAmAClassThatIsNeverUsed {
    // Constructor
    public IAmAClassThatIsNeverUsed() {
        System.out.println("constructor - IAACTINU");
    }

    // Instance init block
    {
        System.out.println("instance - IAACTINU");
    }

    // Static init block
    static {
        System.out.println("static - IAACTINU");
    }
}

正如类名所示,我在任何地方都没有引用新类。 新程序产生了与旧程序相同的输出。


1
请看这里(超简洁和清晰):http://blog.sanaulla.info/2008/06/30/initialization-blocks-in-java/ - Benj
1
这里的一个重要发现是,在子类实例初始化程序之前,父类构造函数将被执行! - ACV
8个回答

100
请参阅JLS version 8的第12.4节和第12.5节,它们详细介绍了所有这些内容(静态变量使用第12.4节,实例变量使用第12.5节)。
对于静态初始化(第12.4节):
在以下任何一种情况发生之前,都将立即初始化类或接口类型T:
  • T是一个类,并且T的实例被创建。
  • T是一个类,并且调用了由T声明的静态方法。
  • 分配了由T声明的静态字段。
  • 使用了由T声明的静态字段,并且该字段不是常量变量(§4.12.4)。
  • T是一个顶级类(§7.6),并且在T(§8.1.3)中词法嵌套的断言语句(§14.10)被执行。
(以及几个含糊词条)

2
最佳答案。做得好! - Chip Uni
1
这个答案涵盖了所有内容。它应该被接受为正确答案。 - Timmos
我认为JLS的这一部分不够精确(如果您想,甚至可能略有错误)。第四项应该是:“T声明的静态字段被使用,并且该字段不是具有文字值初始化程序的常量变量。”例如,请参见:http://pastebin.com/1XfczpjR,“C.main”打印出“42”,但根据上述规则,“B”永远不应该被初始化,因为“answer”是一个常量变量(与JSL 4.12.4相比)。 - Steffen Heil
从JLS中可以得知:“原始类型或字符串类型的变量,如果是final修饰,并且被初始化为编译时常量表达式(§15.28),则被称为常量变量。” 因此,变量answer不是常量变量,因为它没有分配编译时常量值。 - Keith Randall
@SteffenHeil:抱歉,那个引用是来自4.12.4。15.28是被该引用所提及的内容。 - Keith Randall
显示剩余3条评论

66

当访问一个类以创建实例或访问静态方法或字段时,该类的静态初始化程序将被运行。

因此,对于多个类来说,这完全取决于运行的代码来导致这些类被加载。


1
我同意。变量声明不会这样做(我认为),除非它是一个带有初始化的静态变量。 - Carl Smotricz
访问静态字段时,如果该字段被声明为final常量,则不会导致类初始化。另外请注意,虽然SomeClass.class不是一个字段,但访问它也不会使SomeClass初始化。 - Timmos
2
你读过Java语言规范吗?当类被加载时,静态初始化不会运行。 - Hot Licks
1
除非该类被另一个类引用,否则它不会被加载,而这可能会在它被初始化之前很长一段时间。 - Hot Licks
@Timmos,我们在多大程度上可以信任这种行为,以便我们真正控制未来Java更新时何时初始化事物?我想基于可预测的初始化顺序来构建我的项目。因此,我猜静态块,在任何初始化发生时,都可以保证这一点。 - Aquarius Power
显示剩余7条评论

37

Keith和Chris的回答都很好,我只是为了我的具体问题增加一些细节。

静态初始化块按照它们所在类的初始化顺序运行。那么,这个顺序是怎样的呢?根据JLS 12.4.1规定:

在T被以下任意一个条件首次满足前,类或接口类型T将被立即初始化:

  • T是一个类并且T的实例被创建。
  • T是一个类并且T声明的一个静态方法被调用。
  • T声明的一个静态字段被赋值。
  • T声明的一个静态字段被使用,并且该字段不是一个常量变量(§4.12.4)。
  • T是一个顶层类,并且在T中词法嵌套的断言语句(§14.10)被执行。

在Class类和java.lang.reflect包中调用某些反射方法也会导致类或接口初始化。除此之外,类或接口不会在其他情况下初始化。

举个例子,下面是一个演示示例中正在发生的事情:

  1. 输入主函数
  2. 打印"START"
  3. 尝试创建Child的第一个实例,这需要初始化Child
  4. 尝试初始化Child会导致Parent的初始化
  5. 尝试初始化Parent会导致Grandparent的初始化
  6. 在Grandparent的初始化开始时,运行Grandparent的静态初始化块
  7. 从技术上讲,由于Object是Grandparent的父类,因此它对初始化链的最终决定权,但它没有任何贡献
  8. 在Grandparent的静态初始化块结束后,程序回退到Parent的静态初始化块
  9. 在Parent的静态初始化块结束后,程序回退到Child的静态初始化块
  10. 此时,Child已经初始化,因此可以继续执行其构造函数
  11. 由于IAmAClassThatIsNeverUsed从未被引用,因此其代码从未运行,包括静态初始化块
  12. 本教程的其余部分不涉及静态初始化器,仅出于完整性而包含
  13. Child的构造函数隐式调用super()(即Parent的构造函数)
  14. Parent的构造函数隐式调用super()(即Grandparent的构造函数)
  15. Grandparent的构造函数也是如此,这没有任何效果(再次强调,Object没有任何贡献)
  16. 在Grandparent的构造函数的super()调用之后,立即出现Grandparent的实例初始化块
  17. Grandparent的构造函数的其余部分运行并终止构造函数
  18. 程序回退到Parent的构造函数,在其调用super()(即Grandparent的构造函数)解析后立即执行
  19. 与上面类似,Parent的实例初始化器完成其任务,并完成构造函数
  20. 同样地,程序返回并完成Child的构造函数
  21. 此时,对象已经被实例化
  22. 打印"END"
  23. 正常终止

2
在12.4.1中,我们还发现:在类初始化之前,必须先初始化其直接超类... - user85421

3

有一种情况下,静态块不会被调用。

class Super {
    public static int i=10;
}
class Sub extends Super {
    static {
        system.out.println("Static block called");
    }
}
class Test {
    public static void main (String [] args) {
        system.out.println(Sub.i);
    } 
}

上述代码输出 10

来自"编辑"的更新

这个技术解释在JLS 12.4.1中。

"对静态字段的引用(§8.3.1.1)只会导致实际声明它的类或接口的初始化,即使它可能通过子类、子接口或实现接口的类的名称进行引用。"

直观的解释是Super.iSub.i实际上是同一个变量,Sub中的任何内容都不需要被初始化,Super.i就可以获得正确的值。

如果Super.i的初始化表达式指向Sub类,情况就会不同。但这样做会导致初始化顺序中出现循环。仔细阅读JLS 12.4.1JLS 12.4.2可以解释为何允许这样做,并且可以让您准确地了解实际发生的情况。


1

类的初始化包括执行其静态初始化程序和在类中声明的静态字段(类变量)的初始化程序。

接口的初始化包括执行在接口中声明的字段(常量)的初始化程序。

在类初始化之前,必须先初始化其直接超类,但是不会初始化由类实现的接口。同样,接口被初始化之前不会初始化其超级接口。


0
class A {
  public A() { 
    // 2
  }
}

class B extends A{
  static char x = 'x'; // 0
  char y = 'y'; // 3
  public B() { 
    // 4
  }

  public static void main(String[] args) {
    new B(); // 1
  }
}

注释中的数字表示计算顺序,数字越小,越早计算。

就像这个示例一样,

  1. 静态变量
  2. 主函数
  3. 超类的构造函数
  4. 实例变量
  5. 构造函数

0

在同一个类中,您可以拥有多个静态和实例初始化程序,因此

  • 静态初始化程序按照它们声明的文本顺序调用(来自12.4.2
  • 实例初始化程序按照它们声明的文本顺序调用(来自12.5

每个初始化程序都会被执行,就像它是一个单独的块。


-1

http://docs.oracle.com/javase/tutorial/java/javaOO/initial.html

请查看Java文档。
然后明确指出,无论有多少静态块,它们都将作为单个块按照它们出现的顺序执行。
因此,
我在这里的理解是Java正在查看您的代码,如下所示:
static{
i=1;
i=2;
}

static int i;

这就是为什么你得到输出2的原因。

希望这有帮助。


这只是重复了问题所指示的原帖已经知道的信息。 - Stephen C

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