类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。
Java中赋值顺序:
-
父类的静态变量赋值
-
自身的静态变量赋值
-
父类成员变量赋值和父类块赋值
-
父类构造函数赋值
-
自身成员变量赋值和自身块赋值
-
自身构造函数赋值
给出一段代码,请问输出结果是什么。
public class Test {
public static void main(String[] args) {
staticFunction();
}
static Test test = new Test();
static {
System.out.println("1");
}
{
System.out.println("2");
}
Test() {
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() {
System.out.println("4");
}
int a = 110;
static int b = 112;
}
正常来说,如果套用java的赋值顺序来思考的话,输出结果应该是1(静态块)和4(主函数)
但是实际的输出结果却是2,3,a=110,b=0,1,4。怎么输出的结果跟预想的不一样呢?
这里就涉及到了一个关键点:实例的初始化不一定是在类初始化结束之后才开始的。
在类的生命周期内,只有准备阶段和初始化阶段才会涉及到类变量的初始化和赋值。在准备阶段,jvm会为类变量分配内存并赋默认值,类变量test默认为null,静态变量b默认为0。如果b是由final修饰的静态变量,那么b的值在初始阶段便为112。当进行到类的初始化阶段,此时jvm需要执行类构造器,包括类的构造方法和对象的构造方法。因此在此阶段首先执行的是静态赋值语句(按顺序从上至下执行)——static Test test = new Test(),此时实例化对象test,初始化对象的成员变量a=110,然后会调用方法内的构造块,输出2,再调用方法内的构造方法Test(),输出3,a=110,b=0。然后再往下执行静态方法输出1,最后再运行主函数输出4。
看到这里,大家都可能会有疑问,为什么实例的初始化竟然在静态的初始化之前呢?其实实例的初始化并没有出现在静态初始化之前,认真看看流程我们可以发现,在静态初始化Test的时候遇到的test成员是本类的实例,因此在实例化test变量时,实际上是把实例初始化嵌入到静态初始化的流程中。这才是关键所在。