借用一个小例子来分析Java程序的初始化过程,其中涉及类的加载,初始化顺序
class Insect {
private int i = 9;
protected int j;
Insect() {
System.out.println("i = " + i + ", j = " + j);
j = 39;
}
private static int x1 = printInt("static Insect.x1 initialized");
static int printInt(String s) {
System.out.print(s);
System.out.println(". Insect.x1 = " + x1 );
return 47;
}
}
public class Beetle extends Insect {
private int k = printInt("Beetle.k initialized");
public Beetle() {
System.out.println("k = " + k);
System.out.println("j = " + j);
}
private static int x2 = printInt("static Beetle.x2 initialized");
public static void main(String[] args) {
System.out.println("Beetle constructor");
Beetle b = new Beetle();
}
}
以下对程序的分析,如有错误,不吝指教。
(1)如果要运行这段程序,启动虚拟机时,指定要Beetle(包含main()方法)作为启动的类,虚拟机会先初始化Beetle类。
(2)当初始化一个类时,发现其父类尚未初始化,则需要先触发其父类的初始化,所以需要先初始化Insect(注意:这里的初始化指的是类加载过程中的初始化阶段,不是new 类的对象);
(3)在初始化的过程中,会对static的属性进行赋值,也就是Insect.x1。在类的加载过程中分为以下几个阶段:加载-->验证-->准备-->解析-->初始化。在准备阶段会对类变量(注意是类变量,static修饰的变量)进行内存分配,静态变量的初始值是零值。此时Insect.x1 = 0;
(4)现在Insect进行到了加载过程的初始化阶段,会对Insect.x1赋值,
x1 = printInt("static Inject.x1 initialized");
此时,Insect.x1 = 47。Insect类加载完毕
(5)加载Beetle类,同样是在准备阶段将Beetle.x2置零,初始化阶段对
Beetle.x2 = printInt("static Beetle.x2 initilaize");此时,Beetle.x2 = 47。Beetle类加载完毕。
(6)开始执行main()方法,第一条语句是打印;
(7)执行第二条语句,创建Beetle对象b。子类对象会包含一个父类的子对象,这个对象与你用父类直接创建的对象是一样的,区别就是一个被包装在子类的内部,一个在外部。所以首先会调用父类相应的构造方法,创建父类的对象,由于Beetle没有显式的调用父类的构造方法,虚拟机会使用父类的无参构造方法。
(8)这个时候相当于执行:new Insect()。会执行一下动作:
a.首先会在堆上分配一块内存;
b.然后这块存储空间会被置零,这就自动的将Insect类里面的所有属性变成零值(注意,Java要求:在调用任何方法之前,所有属性都至少有一个初值。正是通过置零存储空间来保证这条规则的)。由于x1是类变量,并不存储在这块在堆上分配内存,x1的值仍然为47,i=0,j=0;
c.执行所有出现字段定义处的初始化动作,也就是private int i=9。此时,x1=47,i=0;j=0;
d.执行构造函数;
创建父类对象完成,Insect.x1 = 47,i=9,j=39。
(9)创建子类Beetle的对象,如第八步一般,k的值被置零,k=0;接着执行k定义处的初始化,k=47;执行构造函数;
至此,程序运行完毕,程序的输出结果是:
/********
//验证第三步中所说,在准备阶段给x1赋零值。
static Insect.x1 initialized. Insect.x1 = 0
static Beetle.x2 initialized. Insect.x1 = 47
Beetle constructor
i = 9, j = 0
Beetle.k initialized. Insect.x1 = 47
k = 47
j = 39
********/