父类静态成员变量->父类静态代码块->子类静态成员变量->子类静态代码块->父类非静态成员变量->父类非静态代码块->父类构造函数->子类非静态成员变量->子类非静态代码块->子类构造函数。
父类早于子类,静态早于非静态,非静态早于构造函数。
其实就是java中类加载时初始化的顺序。
只是从代码层面进行非常简略的解读,没有深入到jvm哦。
上次有个例子是这样的:
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 27 28 29 30 31 32 33 34 35 36 37 38 |
class Insect { int i = 9; int j; Insect() { prt("i = " + i + ", j = " + j); j = 39; } static int x1 = prt("static Insect.x1 initialized"); static int prt(String s) { System.out.println(s); return 47; } } public class Beetle extends Insect { int k = prt("Beetle.k initialized"); Beetle() { prt("k = " + k); prt("j = " + j); } static int x2 = prt("static Beetle.x2 initialized"); static int prt(String s) { System.out.println(s); return 63; } public static void main(String[] args) { prt("Beetle constructor"); Beetle b = new Beetle(); } } |
运行结果:
1 2 3 4 5 6 7 |
static Insect.x1 initialized static Beetle.x2 initialized Beetle constructor i = 9, j = 0 Beetle.k initialized k = 63 j = 39 |
其实我觉得这个过程看得还是不太直白,我另外写几个例子进行验证类加载的过程。
一、首先要明白概念
- 虚拟机在首次加载java类时,会对静态初始化块、静态成员变量、静态方法进行一次初始化。
- 只有在调用new方法时才会创建类的实例。
- 类实例的创建过程:按照父子继承关系进行初始化,首先执行父类的初始化块部分,然后是父类的构造方法;再执行本类继承的子类的初始化块,最后是子类的构造方法。
- 类实例销毁的时候,首先销毁子类部分,再销毁父类部分。(这一点暂时不验证,因为要不停创建对象来吸引GC销毁堆中的对象,才能通过finalize()方法看出来,我不想弄得太复杂了(其实就是懒))。
二、接下来验证一下
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 27 28 29 30 31 32 33 34 |
package testClass; public class parent { static String parent1 = "父类的静态变量"; { System.out.println("父类非静态代码块"); } static { System.out.println("static块中的" + parent1); System.out.println("父类静态代码块"); } public static void method1() { System.out.println("method1中的" + parent1); System.out.println("父类静态方法1"); } public static void method2() { System.out.println("父类静态方法2"); } public parent() { // TODO Auto-generated constructor stub System.out.println("构造方法中的" + parent1); System.out.println("父类的构造方法"); } public static void main(String[] args) { parent.method1(); } } |
运行结果如下:
1 2 3 4 |
static块中的父类的静态变量 父类静态代码块 method1中的父类的静态变量 父类静态方法1 |
这里是直接调用static method1()方法的情况,并没有创建对象。
我的理解:
- 首先,依次输出了“static块中的父类的静态变量”和“父类静态代码块”。说明加载过程中初始化了parent类中的static块,因为字段parent1不为null,说明这个字段先于static块中被初始化。
- 然后,依次输出了“method1中的父类的静态变量”和“父类静态方法1”。说明method1方法此时被调用,而且字段parent1此时已经存在。因为输出顺序的缘故,method1方法在初始化static代码块之后执行。
- 最后,我们发现没有构造函数执行。说明这个方法调用的过程没有真正生成对象,只是到执行了方法就结束了。(因为static方法是可以直接在类中调用的)。
static修饰的方法是类的方法,不需要创建对象就可以调用,而非静态方法,只有对象被创建了,才可以调用方法。
直接调用static方法,步骤如下:
- 先初始化方法中的静态成员变量。
- 然后初始化static块。
- 然后调用方法。
- 之后直接结束,没有创建对象。
三、做对比,改写一下main方法,真正创建一个parent对象
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 27 28 29 30 31 32 33 34 35 |
package testClass; public class parent { static String parent1 = "父类的静态变量"; { System.out.println("父类非静态代码块"); } static { System.out.println("static块中的" + parent1); System.out.println("父类静态代码块"); } public static void method1() { System.out.println("method1中的" + parent1); System.out.println("父类静态方法1"); } public static void method2() { System.out.println("父类静态方法2"); } public parent() { // TODO Auto-generated constructor stub System.out.println("构造方法中的" + parent1); System.out.println("父类的构造方法"); } public static void main(String[] args) { parent parent = new parent(); parent.method1(); } } |
其实就是从“直接调用方法”改成了“新建一个对象并调用其方法”
运行结果如下:
1 2 3 4 5 6 7 |
static块中的父类的静态变量 父类静态代码块 父类非静态代码块 构造方法中的父类的静态变量 父类的构造方法 method1中的父类的静态变量 父类静态方法1 |
输出结果变多了,因为是真正创建一个Parent对象,所以初始化步骤也变多了。
我的理解:
- 首先,输出了“static块中的父类的静态变量”和“父类静态代码块”。和上个例子一样,先初始化了字段parent1,然后初始化了static块。
- 其次,输出了“父类非静态代码块”。说明此时初始化了非静态代码块。
这里有我的理解,不知道是否正确,可能需要回头再校对。
到了这一步,类加载已经完成了。
静态成员都已经加载完了,已经在内存中分配了空间。此时,必要的类已全部装载完毕(类加载完成了),所以能够创建对象。
首先,这个对象中的所有基本数据类型都会设成它们的默认值,而将对象句柄设为null。随后会调用基础类构建器。在这种情况下,调用是自动进行的。但也完全可以用super来自行指定构建器调用。
下面的步骤是在类初始化过程中完成的。
- 然后,输出了“构造方法中的父类的静态变量”和“父类的构造方法”。说明此时执行了构造方法,而且字段变量是先于构造方法初始化的。
- 之后,输出了“method1中的父类的静态变量”和“父类静态方法1”。说明此时执行了method1方法。
那么问题来了,这里的static方法是什么时候进行初始化的呢?
后来一想才知道,虽然不被调用就不会执行,但是静态方法也属于静态成员变量,和静态变量一样,在类加载的时候就会进行初始化了。
新建对象,步骤如下:
- 先执行类加载。
- 类加载过程中初始化static部分。
- 类加载完成后,进行对象的创建,初始化所有的非static部分。
- 之后完成创建。
四、接下来就引入继承
引入了继承,对象的创建过程就有变化了。主要体现在类加载顺序和对象初始化顺序上。
改写程序:parent.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 25 26 |
package testClass; public class parent { { System.out.println("父类非静态代码块"); } static { System.out.println("父类静态代码块"); } public static void method1() { System.out.println("父类静态方法1"); } public static void method2() { System.out.println("父类静态方法2"); } public parent() { // TODO Auto-generated constructor stub System.out.println("父类的构造方法"); } } |
son.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 25 26 27 28 29 30 |
package testClass; public class son extends parent { { System.out.println("子类非静态代码块"); } static { System.out.println("子类静态代码块"); } public static void method1() { System.out.println("子类静态方法1"); } public static void method2() { System.out.println("子类静态方法2"); } public son() { // TODO Auto-generated constructor stub System.out.println("子类的构造方法"); } public static void main(String[] args) { son son = new son(); } } |
创建一个新的son对象,运行结果如下:
1 2 3 4 5 6 |
父类静态代码块 子类静态代码块 父类非静态代码块 父类的构造方法 子类非静态代码块 子类的构造方法 |
我的理解:
- 首先,输出“父类的静态代码块”。说明先初始化父类中的静态代码块。如果父类依然有父类,那么以此类推。
- 其次,输出“子类静态代码块”。说明这时候初始化了子类中的静态代码块。千万不要想当然,java中的类不是顺序加载的。很多人可能认为:在子类加载过程中发现了继承了父类,那么先完成父类中的类加载,再来完成子类中的类加载。
事实上,think in java中提到:
若基础类含有另一个基础类,则另一个基础类随即也会载入,以此类推。接下来,会在根基础类执行static初始化,再在下一个衍生类执行,以此类推。保证这个顺序是非常关键的,因为衍生类的初始化可能要依赖于对基础类成员的正确初始化。
可以看出,是先加载所有基础类,然后从跟基础类开始逐一执行static的初始化。
这里有我的理解,不知道是否正确,可能需要回头再校对。
因为static的初始化是类加载的步骤中执行的,当所有static初始化完毕,代表所有static部分都正确分配了内存,所有类加载已经结束了。这时候可以开始进行对象的创建(初始化)。没有static修饰的实例变量,只有对象被创建了,才会分配内存空间,每一个对象的实例变量互不相关。
- 然后,输出了“父类非静态代码块”和“父类的构造方法”。因为使用了构造函数,所以此时parent对象已经被创建了。
- 之后,输出了“子类非静态代码块”和“子类的构造方法”。同上,此时son对象已经被创建了。
引入继承的新建对象,步骤如下:
- 先执行类加载,类加载过程中发现继承了父类,于是加载父类,直至加载到根基础类为止。
- 类加载的过程中进行static成员变量和static代码块的初始化。顺序是:先初始化父类的static成员变量,再初始化父类的static代码块,再初始化子类的static成员变量,再初始化子类的static代码块。
- 这时候,所有类都加载完成了,开始初始化对象。先初始化父类非静态成员变量,再初始化父类非静态代码块,再使用父类构造函数。
- 接下来是初始化子类非静态成员变量,再初始化子类非静态代码块,再使用子类构造函数…
以此类推,直至完成所有对象的创建。
总结步骤如下:
父类静态成员变量->父类静态代码块->子类静态成员变量->子类静态代码块->父类非静态成员变量->父类非静态代码块->父类构造函数->子类非静态成员变量->子类非静态代码块->子类构造函数。
五、总结
这次从代码层面研究了类加载和初始化的的过程。如果之后感觉有问题,我会赶紧回来修改。