最近在学习Java类的加载机制遇到了这样的一个题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class InitializeOrder {

public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();

System.out.println("counter1: " + Singleton.counter1);
System.out.println("counter2: " + Singleton.counter2);
}
}

class Singleton {
public static int counter1;
public static int counter2 = 0;

private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}

public static Singleton getInstance() {
return singleton;
}
}

答案:

1
1

修改一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InitializeOrder {

public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();

System.out.println("counter1: " + Singleton.counter1);
System.out.println("counter2: " + Singleton.counter2);
}
}

class Singleton {
public static int counter1;

private static Singleton singleton = new Singleton();
private Singleton() {
counter1++;
counter2++;
}

public static int counter2 = 0;

public static Singleton getInstance() {
return singleton;
}

}

答案:

1
0

对于第一段程序,相信大家都能说出正确答案,但是对于第二段程序,大家就不一定能够回答正确(至少像我这样的蒟蒻回答错误了)。
 稍微思考了一下,终于理解这是为什么了。
 我们知道,类的加载过程会经历 加载、验证、准备、解释、初始化、使用与卸载这几个阶段。顺被复习一下:加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,但并不是按部就班地“进行”或完成,因为这些阶段都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。。:)
 回到正题。首先第一行Singleton singleton = Singleton.getInstance(); 调用静态方法,造成Singleton类的主动使用,导致Singleton类的初始化。而在初始化之前,加载、验证、准备阶段已经开始了。
 在准备阶段,类变量被赋予了默认值(零值),counter1被赋值为0, singleton被赋值为了null,counter2被赋值为了0。到此为止,第一段程序与第二段程序并没有什么不同。
 而到了初始化阶段,对于第一段程序,程序先给counter2赋值为0,然后 new 了一个对象赋值给了singleton,在实例化的过程中,调用了默认构造函数,将counter1、counter2都进行了加一操作。这时counter1 = 1, counter2 = 1。所以 最后打印的也就是 1 1了。
 而对于第二段程序,程序先 new 了一个对象赋值给了singleton,在实例化的过程中,调用了默认构造函数,将counter1、counter2都进行了加一操作。这时counter1 = 1, counter2 = 1。接着进行下面的操作public static int counter2 = 0,程序将counter2赋值为零。所以最后打印的答案就变成了 1 0。

接下来我们再来看一段程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Initialize {
public static void main(String[] args) {

System.out.println(Singleton.a);
}

}

class Singleton {
public static int a = 1;

static {
a = 5;
}

}

答案:

5

接着修改一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Initialize {
public static void main(String[] args) {

System.out.println(Singleton.a);
}

}

class Singleton {

static {
a = 5;
}

public static int a = 1;
}

答案:

1

有了上面的分析,相信得到正确答案并不难。对于第三段程序,在初始化阶段先给a 赋值为了1,然后在静态代码块中赋值为了5。而对于第四段程序,在初始化阶段,先在静态代码块中给a赋值为5,然后再下面的代码中给a 赋值为1。
 如果在第四段程序中,我们不给 a 赋值,那么最后的答案是什么呢。其实这样更简单,程序在初始化阶段只在静态代码块中给a 赋值为5,所以最后的答案当然是 5 啦。
 需要要注意的是,静态语句块中只能访问到定义在静态语句块之前的变量,而对于定义在它之后的变量,只能赋值,不能访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Initialize {
public static void main(String[] args) {

System.out.println(Singleton.a);
}

}

class Singleton {

static {
a = 5;
System.out.println(a);// 编译器会提示“非法向前引用”
}

public static int a = 1;
}

由此可见,类的初始化是按照从上到下的顺序的。于此同时,我们也看到了准备阶段的重要性。如果没有准备阶段,在第二个程序中,在初始化阶段对counter2的加一操作可能会直接导致报错。
 最后我们再来看一题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Initialize {

public static void main(String[] args) {
System.out.println(SubChild.b);
}
}

class SuperParent {
public static int a = 1;
static {
a = 2;
}
}

class SubChild extends SuperParent {
public static int b = a;
}

答案

2
 因为虚拟机会保证子类的()方法执行之前,父类的()方法已经执行完毕。所以在初始化子类的时候,父类已经初始化完毕了,这时的a 为2,所以b也被赋值为了2,所以打印出的就是2.

总结一下类初始化顺序:先初始化完父类,在初始化子类。初始化是从上至下的。