Xudong's Blog

Java中对象域的初始化

Word count: 1.2kReading time: 5 min
2018/10/03

正确的初始化很重要

java中,一个类的域(或者说类的属性,类的数据成员)可以是基本数据类型,也可以是对象的引用。对象域如果没有被初始化,会被置为null,如果在置为null的引用上调用方法,我们就会得到一个运行时错误。
在恰当的时间,恰当的位置,正确地初始化对象域是很重要的。

初始化的位置

java中可以在四个位置进行类的对象域初始化:

  • 在域定义时初始化
  • 在类构造器中初始化
  • 在使用类中的对象域之前手动初始化(这被称作懒惰初始化)
  • 使用实例初始化块

要注意上面列出的顺序并不是这些位置初始化的时间顺序。我们经常能看到的初始化方式是1和2。

当然,我们不能忘了一个特殊的存在:静态成员。
如果一个对象域是静态的,那么它的初始化就和类对象的初始化剥离开了。
静态成员的初始化在类被加载时进行,这是我们一定要知道的,但不是这篇文章的重点。

初始化的顺序

下面通过代码来证明这四种初始化发生的时间顺序。
类A拥有一个静态数据成员sstrA,和一个数据成员strA。拥有一个静态初始化块,和一个类初始化块。有一个无参构造函数。override了toString方法,在toString方法中懒惰初始化strA。
而类B结构与A基本相同,并且继承自A。

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
54
55
56
57
58
59
60
61
62
63
64
65
    class A {
static String sstrA = "static string in A";
String strA = "string in A"; //在定义时初始化

static { //静态初始化块
System.out.println("A static init block start");
sstrA = "sstrA from init block";
}

{ //实例初始化块
System.out.println("instance init block start");
strA = "strA from init block";
}

public A() { //构造函数中可以做初始化
System.out.println("constructor start");
}

@Override
public String toString() {
if (strA == null) //这里使用了懒惰初始化
strA = "strA from delayed init";
return "static sstrA: " + sstrA + "\nstrA: " + strA;
}
}

class B extends A {
static String sstrB = "static string in B";
String strB = "string in B";

static {
System.out.println("B static init block start");
sstrB = "sstrB from init block";
}

{
System.out.println("instance init block start");
strB = "strB from init block";
}

public B() {
System.out.println("constructor start");
}

@Override
public String toString() {
if (strB == null)
strB = "strB from delayed init";
return super.toString() + "\nstatic sstrB: " + sstrB + "\nstrA: " + strB;
}
}
```java

在main函数中创建这两个类的实例:

```java
A a = new A();
System.out.println("----------------");
System.out.println(a);

System.out.println("\n================\n");

B b = new B();
System.out.println("----------------");
System.out.println(b);

可以得到输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A static init block start
A instance init block start
A constructor start
----------------
static sstrA: sstrA from init block
strA: strA from init block

================

B static init block start
A instance init block start
A constructor start
B instance init block start
B constructor start
----------------
static sstrA: sstrA from init block
strA: strA from init block
static sstrB: sstrB from init block
strA: strB from init block

根据前半部分的运行结果,我们看到首先在类加载时,静态初始化块被执行了。
然后便是类实例化创建对象的过程,在构造函数被调用前,实例初始化块被首先执行。
根据toString的返回字符串,我们看到显示的内容是实例初始化块中赋值的字符串,说明最一开始在定义处初始化的字符串被实例初始化块覆盖掉。
最终得出一个顺序:

  • 在最一开始,类被加载时,静态相关的初始化会执行
  • 类实例化的第一步,定义处的初始化会被执行
  • 然后是实例初始化块的执行
  • 再后面才是构造器
  • 最后当然是懒惰初始化在必要时得到执行

然后后半部分B类的实例化过程展现了继承存在时初始化的顺序。

因为A类在之前已经被加载过,所以静态初始化块不会被执行,此处只有B类的静态初始化块。

在实例化过程中首先会进行父类A的初始化,然后才是B本身的初始化。

总的来说,java在类成员初始化的顺序上,并没有什么反直觉的地方,只要稍加理解便能掌握。

尾巴

其实,对象域的初始化甚至可以发生在对象的外部,初始化的时间也可以由运行时决定,反射技术将可以达到这一目的,我们可以将其大致归类到惰性初始化中。对象的初始化还可以更深入到JVM层面来剖析,这篇浅显的文章就没有展开来讲。Java中的每个点都有更深层次的细节可以学习,希望我们可以一同探索。

CATALOG
  1. 1. 正确的初始化很重要
  2. 2. 初始化的位置
  3. 3. 初始化的顺序
  4. 4. 尾巴