借用一个小例子来分析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=9;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
********/
============================================================================================
看了上面的内容,觉得还不够完整,再补充一点关于静态语句块和静态变量、子类初始化的内容。
class Print {
//为了演示静态语句块和静态属性的初始化是按照定义的先后顺序而设的
public Print() {
System.out.println("class Print is created.");
}
//为了演示构造子类之前,先调用父类的构造方法
public Print(String className) {
System.out.println("class Print is create by " + className);
}
}
class Father {
private Print p1 = new Print("Father");
static {
System.out.println("Before static field init." +
"\n----------------" );
//在类变量定义之前的静态语句块不可以使用,编译无法通过
//System.out.println(p);
//但是可以在静态语句块中对其进行赋值,这样是没问题的
//p = new Print();
//即使已经定义了,也不能使用
//Cannot reference a field before it is defined
//System.out.println(p);
}
static Print p = new Print();
static {
System.out.println("----------------\n" +
"After static field init.");
}
public Father(int i) {
System.out.println("class Father is created ");
}
}
class Son extends Father {
private Print p = new Print("Son");
public Son() {
//父类的构造方法要放在有效代码的第一行
super(0);
System.out.println("class Son is created");
}
public static void main(String[] args) {
new Son();
}
}
/*output*/
Before static field init.
----------------
class Print is created.
----------------
After static field init.
class Print is create by Father
class Father is created
//以上两行的输出说明先调用父类的构造方法
class Print is create by Son
class Son is created
从上面的输出中,我们可以得到以下结论:
1)静态语句块和静态属性的初始化顺序是按照定义的先后顺序来进行的,但是都早于非静态属性的初始化(其实这是句废话,看了上面一段的就明白了)。在静态属性定义之前的静态语句块可以对其赋值,但是不能是使用(即使已经初始化了,仍然不能使用,记住:只有在定义处之后才能使用)。
2)我们知道,当子类继承父类时,子类已经知道了父类的一切,并且可以访问父类中任何声明为public和protected的成员。这意味着,必须假定父类的所有成员都是有效的。为了确保这一目的,唯一的办法就是:首先调用父类的构造器。在调用父类构造器之前,自然会执行父类属性定义处的初始化(又说了一句废话)。
3)关于构造方法:
a.一个类如果没有构造方法,编译器认为:这个类需要构造方法,给它提供一个默认无参构造方法,这个构造方法什么也不做;
b.这个类如果有了构造方法,无论有无参数,编译器都会认为:这个类有构造方法,它知道自己要做什么,所以不需要我操心了,不要给它提供任何额外的构造方法了。
关于子类构造方法:子类构造方法必须在第一行(不包括注释)使用关键字super调用父类的构造方法,否则编译器默认调用父类的无参构造方法。关于这句话的解释是这样的:如果父类有无参构造方法,无论是自己写的,还是编译器默认创建的,那么可以不不使用super(),当然也可以使用;如果父类没有无参构造方法,那就必须使用super(参数)来显式调用,否则编译无法通过。
以上内容均为原创,转载请注明:http://blog.csdn.net/yuhongye111/article/details/25502057