【java天梯-2】类的初始化顺序

概述

1
Object o = new Object()

上述代码为使用new操作创建一个Object类型的对象,在该对象从无到有的过程中,到底经历了哪些过程?按照什么顺序执行的?

属性

类中定义的属性分为静态和非静态两种。

其中静态属性与类对象无关,只与类有关,即只与Class有关,一经初始化在该类的所有对象中仅此一份。

而非静态属性需要依赖于该类的对象,即该类的每一个对象都持该属性,每个对象的该属性独立于其他对象存在。

类中仅定义但未手动提供默认值的属性(eg: private String username; )会在类加载后被赋予默认值,何为默认值呢?

  • 类属性为引用对象类型时,初始化默认值为null。包装类型和String类型也在其中
  • 类属性为基本类型中的数值类型时,初始化默认值为0
  • 雷属性为基本类型中的boolear类型时,初始化默认值为false
    另外一种定义且手动提供默认值的属性(eg:private String username = “admin”; )会直接被赋予手动指定的初值。

构造函数

构造函数在执行 new Object() 时被调用,在构造函数内可以调用父类构造函数、对属性赋值、执行自定义类的初始化操作

构造块

构造块分为静态和非静态两种,其功能均为完成一组初始化操作。静态构造块在非静态构造块之前执行,与出现的顺序无关。eg:

1
2
3
4
5
6
7
{
System.out.println("非静态构造块");
}

static {
System.out.println("静态构造块");
}

Code

了解了类初始化过程中的一些角色之后,看一段代码,分析一下类初始化中各个模块的执行顺序是怎样的。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Test {

private A a = new A();

{
System.out.println("Test 类的非静态构造块被执行");
}

public static void main(String[] args) {
Test test = new Test();
}

static A a2 = new A(2);

static {
System.out.println("Test 类的静态构造块被执行");
}
}

class A {

static B b1 = new B();

private Integer n = 0;

public A() {
System.out.println("默认初始化类A, n= " + n);
}

public A(Integer n) {
System.out.println("使用 n= " + n + " 初始化类A");
this.n = n;
}
}

class B {

private A a5 = new A(5);

public B() {
a5 = new A();
}
}

// 输出:
/*
使用 n= 5 初始化类A
默认初始化类A, n= 0
使用 n= 2 初始化类A
Test 类的静态构造块被执行
默认初始化类A, n= 0
Test 类的非静态构造块被执行
*/

当执行Test类的main方法运行该程序时,在main方法体内并没有输出语句,那为何会有这样的输出结果呢?其实在new一个对象的时候,只把构造函数作为类初始化的唯一要素是不够的,因为在执行构造函数之前,类的初始化小组已经干了许多活了。

什么?执行构造函数之前还有操作?做了啥事儿?

让我们在main方法体第一行添加一个输出语句 System.out.println("main函数开始执行"); ,输出结果为:

1
2
3
4
5
6
7
使用 n= 5 初始化类A
默认初始化类A, n= 0
使用 n= 2 初始化类A
Test 类的静态构造块被执行
main函数开始执行
默认初始化类A, n= 0
Test 类的非静态构造块被执行

很显然,这段代码在main方法中new对象之前,确实做了不少工作。让我们分析一下该操作的执行顺序。

  1. 首先JVM加载Test类,按代码顺序初始化该类中所有的静态角色(静态属性、静态函数和静态构造块),即先执行Test类中 static A a2 = new A(2); 以2为构造函数参数构造类A对象。
  2. 转到类A,在执行类A构造函数 A(Integer n) 之前,类A被加载到JVM中,按代码顺序初始化该类中所有静态角色,即执行 static B b1 = new B(); ,使用类B的无参构造方法构造类B对象。
  3. 转到类B,在执行类B构造函数 B() 之前,类B被加载到JVM中,按代码顺序初始化该类中所有静态角色,现在类B中无静态角色,此时执行类属性初始化操作,即执行 private A a5 = new A(5); 以5为构造函数参数构造类A对象
  4. 转到类A,因第2步中类A已经被JVM加载,静态属性已经进入初始化队列中,无需再次初始化,所以直接转到初始化类A的非静态属性,即 private Integer n = 0;
  5. 接着,执行类A带有一个Integer参数的构造函数,将5传入,输出使用 n= 5 初始化类A,并将n赋值为5
  6. 此时第三步中类B的属性初始化执行完毕,回到类B,执行类B的无参构造方法。在该构造函数内部,再次调用类A的无参构造函数构造类A的对象
  7. 转到类A,因类A的静态属性已经初始化完毕,但是其非静态属性需要依赖于类对象,所以执行类A非静态属性的初始化,n=0,输出 默认初始化类A, n= 0 ,重新赋值给B的属性a5。
  8. 此时第2步中,真正去执行类A的构造函数A(Integer n),传入参数2,构造类A对象。输出 使用 n= 2 初始化类A 。至此,Test类第一个静态角色才被执行完毕
  9. 转到Test类,执行第二个静态角色,静态构造块,输出 Test 类的静态构造块被执行 。此时Test类所有静态角色被执行完毕
  10. 执行main函数,此时main函数才真正被执行。输出main函数开始执行,并执行 Test test = new Test();
  11. 在执行 Test test = new Test(); 之前,先检查该类静态角色是否已经初始化完毕,当然已经被初始化,那么接着按顺序执行非静态角色,即先执行private A a = new A();构造类A对象
  12. 转到类A,因类A静态角色均初始化完毕,所以执行类A非静态角色初始化,即属性n被赋予初始值0,接着执行类A无参构造方法,输出 默认初始化类A, n= 0
  13. 转到Test,执行非静态构造块,输出 Test 类的非静态构造块被执行
  14. 执行Test类默认构造函数,完成Test类对象的最终构造

再执行main函数时,经过上述15步后,最终的Test类对象test才被new出来。可想而知,Java在new一个对象之前为我们执行了如此之多的初始化动作,所以了解Java对象的初始化确实可以在我们编写程序和解决Bug时带来便利。

类对象初始化顺序

经过上述的例子,总结一下类对象初始化时通过什么顺序,执行了哪些操作。假设有个名为Dog的类

  1. 及时没有显示的使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
  2. 然后载入Dog.class,这将创建一个Class对象。因此静态初始化只在Class对象首次加载的时候进行。即此时执行静态初始化
  3. 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
  4. 这块内存空间会被清零,这就自动的将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔类型和字符类型也相同),而引用则被设置成了null
  5. 执行所有出现于字段定义处的初始化动作,非静态构造块等
  6. 执行构造器。

总结

通过对Java类初始化的学习,进一步了解了对象从无到有的过程,也对Java对象生命周期的前半部分做了一定的了解。

参考

  • 《Java编程思想》 — 初始化与清理