Java类中字段及子类初始化过程详解

  1. 1. 初始化顺序
  2. 2. 静态数据的初始化
  3. 3. 显式的静态初始化
  4. 4. 子类初始化
  5. 5. 子类及静态数据初始化

本文介绍了 Java 类中不同类型的字段以及子类的初始化过程。

1. 初始化顺序

类中的变量定义顺序决定了初始化的顺序,即使分散到方法定义之间,变量定义仍然会在任何方法(包括构造器)调用之前就被初始化,看下面这段代码:

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
package com.yyj;

public class OrderOfInitialization {
public static void main(String[] args) {
B b = new B();
b.f();

/*
* Initial class A with 1
* Initial class A with 2
* Initial class A with 3
* Initial class B
* Initial class A with 4
* This is f()
*/
}
}

class A {
A(int value) {
System.out.println("Initial class A with " + value);
}
}

class B {
A a1 = new A(1); // 在构造器之前定义

B() {
System.out.println("Initial class B");
a3 = new A(4); // 在构造器内定义
}

A a2 = new A(2); // 在构造器之后定义

void f() {
System.out.println("This is f()");
}

A a3 = new A(3); // 在尾部定义
}

在类 B 中,A 对象的定义分散到各个方法之间,但他们均在构造器执行前被初始化完成,其中有一个对象引用在构造器内被重新初始化。

a3 引用被初始化了两次:一次在构造器调用之前,另一次在构造器调用期间(第一个对象被丢弃了,因此稍后可能会被垃圾收集器回收)。

2. 静态数据的初始化

无论创建了多少对象,静态数据都只有一份存储空间。来看下面的代码:

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
package com.yyj;

public class StaticInitialization {
public static void main(String[] args) {
System.out.println("Creating class B in main...");
B b1 = new B();
}

static B b2 = new B();

/*
* Initial class A with 2
* Initial class A with 3
* Initial class A with 1
* Initial class B
* This is A.f()
* Creating class B in main...
* Initial class A with 1
* Initial class B
* This is A.f()
*/
}

class A {
A(int value) {
System.out.println("Initial class A with " + value);
}

void f() {
System.out.println("This is A.f()");
}
}

class B {
A a1 = new A(1);
static A a2 = new A(2);

B() {
System.out.println("Initial class B");
a2.f();
}

static A a3 = new A(3);
}

静态字段 a2a3 的创建在 a1 字段之前,且仅在第一个 B 对象创建时被初始化,之后这些静态对象不会被重新初始化。

因此初始化的顺序是从静态字段开始,然后是非静态字段。例如要执行静态的 main 方法,必须先加载 StaticInitialization 类,然后初始化他的静态字段 b2,这就导致类 B 被加载,而类 B 中包含静态的类 A 的对象,因此 A 也被加载,所以这个程序中所有的类都在 main 方法开始执行前被加载。

现在总结一下对象创建的过程,假设有一个名为 A 的类:

  1. 尽管没有显式使用 static 关键字,但构造器实际上也是静态方法。因此,第一次创建类型为 A 的对象时,或者第一次访问类 A 的静态方法或静态字段时,Java 解释器会搜索类路径来定位 A.class 文件。
  2. A.class 被加载后(这将创建一个 Class 对象),它的所有静态初始化工作都会执行。因此,静态初始化只在 Class 对象首次加载时发生一次。
  3. 当使用 new A() 创建对象时,构建过程首先会在堆上为 A 对象分配足够的存储空间。
  4. 这块存储空间会被淸空,然后自动将该 A 对象中的所有基本类型设置为其默认值(数值类型的默认值是0,booleanchar 则是和0等价的对应值),而引用会被设置为 null
  5. 执行所有出现在字段定义处的初始化操作。
  6. 执行构造器。这实际上可能涉及相当多的动作,尤其是在涉及继承时。

3. 显式的静态初始化

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
31
32
33
34
35
36
37
38
package com.yyj;

public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
B.a1.f();

/*
* Inside main()
* Initial class A with 1
* Initial class A with 2
* This is A.f()
*/
}
}

class A {
A(int value) {
System.out.println("Initial class A with " + value);
}

void f() {
System.out.println("This is A.f()");
}
}

class B {
static A a1;
static A a2;
static {
a1 = new A(1);
a2 = new A(2);
}

B() {
System.out.println("Initial class B");
}
}

尽管看起来有点像一个方法,但它只是在 static 关键字后加了一段代码,这段代码和其他静态初始化语句一样,只执行一次:第一次创建该类的对象时,或第一次访问该类的静态成员时(即使从未创建过该类的对象)。

4. 子类初始化

当创建子类对象时,它里面包含了一个基类的子对象。正确初始化基类的子对象至关重要,我们只有一种方法可以保证这一点:在子类构造器中调用基类构造器来执行初始化,它具有执行基类初始化所需的全部信息和权限。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
package com.yyj;

class A {
A() {
System.out.println("A constructor");
}
}

class B extends A {
B() {
System.out.println("B constructor");
}
}

public class BaseNonParamsConstructor extends B {
BaseNonParamsConstructor() {
System.out.println("BaseNonParamsConstructor constructor");
}

public static void main(String[] args) {
BaseNonParamsConstructor x = new BaseNonParamsConstructor();

/*
* A constructor
* B constructor
* BaseNonParamsConstructor constructor
*/
}
}

可以看到构造过程是从基类“向外”逬行的,因此基类在子类构造器可以访问它之前就被初始化了。即使没有为 BaseNonParamsConstructor 创建构造器,编译器也会为它合成一个可以调用基类构造器的无参构造器。

如果基类没有无参构造器,或者如果你必须要调用具有参数的基类构造器,那么就要使用 super 关键字和相应的参数列表,来显式调用基类构造器:

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
package com.yyj;

class A {
A(int i) {
System.out.println("A constructor");
}
}

class B extends A {
B(int i) {
super(100);
System.out.println("B constructor");
}
}

public class BaseWithParamsConstructor extends B {
BaseWithParamsConstructor() {
super(100);
System.out.println("BaseWithParamsConstructor constructor");
}

public static void main(String[] args) {
BaseWithParamsConstructor x = new BaseWithParamsConstructor();

/*
* A constructor
* B constructor
* BaseWithParamsConstructor constructor
*/
}
}

5. 子类及静态数据初始化

了解整个初始化的过程,包括继承,有助于我们全面了解所发生的事情,现在我们来看一个综合示例:

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
package com.yyj;

class Insect {
private int id;
private static int x = printInit("Static Insect.x initialized"); // 静态字段

Insect() {
System.out.println("Insect constructor");
id = 1;
}

static int printInit(String s) {
System.out.println(s);
return 1;
}
}

public class Beetle extends Insect {
private int k = printInit("Beetle.k initialized");
private static int x = printInit("Static Beetle.x initialized"); // 静态字段

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

public static void main(String[] args) {
System.out.println("Inside main()");
Beetle b = new Beetle();

/*
* Static Insect.x initialized
* Static Beetle.x initialized
* Inside main()
* Insect constructor
* Beetle.k initialized
* Beetle constructor
*/
}
}

当你运行这个文件时,首先会尝试访问静态方法 Beetle.main(),所以加载器会去 Beetle.class 文件中找到 Beetle 类的编译代码,在加载它的代码时,加载器注意到有一个基类 Insect,然后它就会去加载基类。无论是否创建该基类的对象,都会发生这种情况。

如果基类又有自己的基类,那么第二个基类也将被加载,以此类推。接下来,会执行根基类(本例中为 Insect)中的静态初始化,然后是下一个子类,以此类推。这很重要,因为子类的静态初始化可能依赖于基类成员的正确初始化。