事实上,Java中变量的初始化是一件既繁琐又简单的事情。说它简单是因为,在执行方法之前,无论是实例变量或者静态静态都可以被很好的初始化,即使是没有显示的初始化为某个值,Java也会赋予它默认值;说它繁琐在于,Java的一些机制保证变量在不同的时间点进行初始化,而且实例变量和类变量的初始化时机截然不同,如同下面这段代码:

  1. public class Init {
  2. public static int i;
  3. static {
  4. System.out.println("静态变量i初始化之前:" + i);
  5. i = 1;
  6. System.out.println("静态变量i初始化之后:" + i);
  7. }
  8. public static void main(String[] args) {
  9. }
  10. }

可能很多人都会猜到了它的输出结果:

静态变量i初始化之前:0 静态变量i初始化之后:1

但是为什么会产生这个结果呢?这就不得不从类加载器说起,上面我们调用了Init类的静态main方法,如果该类是第一次执行,那么类加载器首先会把Init类的字节码加载到中内存中,下图1是类加载器的执行流程。
image.png

在准备阶段会为类变量(注意这里是被static修饰的变量)分配内存并设置类变量的初始值,实例变量将会在对象实例化时随着对象一起分配在Java堆中,这里所说的初始值是指数据类型的零值,假设有一个类变量的定义为
那么变量value在准备阶段会被初始化为0而不是12,而把value赋值为12的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把静态变量赋值为12将在初始化阶段才执行。
在初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,静态变量已经被赋予初始值,而在初始化阶段,则根据程序员的主观计划去初始化静态变量和其它资源,或者说初始化阶段是执行类构造器()方法的过程,注意这里的()方法并不是构造器方法:

  • ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

    1. public class Init {
    2. static {
    3. j = 1;
    4. System.out.println(j);
    5. }
    6. public static int j;
    7. public static void main(String[] args) {
    8. }
    9. }

    上面提到的定义在静态语句块后边的变量,可以赋值但不能访问,如下面代码所示,为j赋值是没有问题的,但是打印j时程序就会报错:“Cannot reference a field before it is defined”。

  • ()方法与类的构造构造函数不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕,因此在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object,如下面代码所示。

  1. class InitPre {
  2. public static int i;
  3. static {
  4. System.out.println("init i");
  5. }
  6. }
  7. public class Init extends InitPre {
  8. public static int j;
  9. static {
  10. System.out.println("init j");
  11. }
  12. public static void main(String[] args) {
  13. }
  14. }

它的输出结果为:

init i init j

可以看到它先执行的是父类的()方法,然后再执行子类的()方法。
上面提到的是静态变量的初始化,由于它是在类加载时初始化的,而且在通常情况下,一个类只会被加载一次,所以我们说静态变量只初始化一次。当我们执行某个类静态main方法时,如果该类没有加载到内存中,执行类的加载。
以上涉及到的是静态变量的初始化,它初始化的时机是类加载时,这时候就会出现一个问题:在什么样的情况下会去加载类(这里的加载是一个广义的概念,包括加载、验证、准备、初始化等阶段):

  • 遇到new、getstatic、putstatic或者invokestatic这四个字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或者设置一个类的静态字段时以及调用一个类的静态方法时;
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化,譬如当执行下面这行代码时就会对类初始化:
  1. Class initClass = Class.forName("继承.Init");
  • 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其类的初始化;
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类。

    实例变量的初始化时机

    当用new关键字创建对象的时候,首先会在堆上为该对象分配内存空间,这时会将对象中所有的实例变量置零,然后执行所有出现于字段定义处的初始化(包括实例块中初始化动作),后执行类中的构造函数,如下面这段代码所示: ```java class InitPre { public int i = 1; static {
    1. System.out.println("static block InitPre class");
    } {
    1. System.out.println("i:" + i);
    2. i = 2;
    3. System.out.println("i:" + i);
    } public InitPre(){
    1. System.out.println("InitPre constructor");
    }

}

public class Init extends InitPre { public int j; static { System.out.println(“static block Init class”); } { System.out.println(“j:” + j); j = 2; System.out.println(“j:” + j); } public Init(){ System.out.println(“Init constructor”); } public static void main(String[] args) { Init init = new Init(); } } ``` 它的输出结果为:

static block InitPre class static block Init class i:1 i:2 InitPre constructor j:0 j:2 Init constructor