最近在学习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.
总结一下类初始化顺序:先初始化完父类,在初始化子类。初始化是从上至下的。