文章目录
  1. 1. 初始化时机
  2. 2. 初始化步骤
    1. 2.1. 概念分析
    2. 2.2. 执行顺序
  3. 3. 实例理解
    1. 3.1. 实例 1
    2. 3.2. 分析 1
    3. 3.3. 实例 2
    4. 3.4. 分析 2
  4. 4. 后记

在运行 Java 代码时,很多时候需要弄清楚程序执行的流程,而面向对象的 Java 程序并非像主要为面向过程而设计的 C 语言一样去顺序执行(简单按照代码的顺序),这使得对于类文件的加载以及执行流程的理解非常重要。本文简单介绍了 Java 类的初始化部分具体过程,包括成员变量、静态代码块、构造函数等的初始化时机及执行流程。

初始化时机

根据 javase 8 的文档说明[1],一个类(本文暂不考虑接口)T 将在下列情况第一次出现前立即被初始化:

  • T 的一个实例被创建[2]
  • T 的一个静态方法被调用;
  • T 声明的一个静态变量被赋值;
  • T 声明的一个静态变量被使用并且这个变量不是常量[3]
  • 暂不考虑这种情况(涉及到顶层类,断言,内部类等)

另外要注意:当一个类初始化时,它的父类如果没有初始化则会先被初始化。

初始化步骤

首先要弄清楚几个基本概念:静态代码块、构造代码块、构造方法、成员变量、子父类的初始化。

概念分析

  • 静态代码块, 一般用于类的数据初始化,形式为:
static block demo
1
2
3
4
5
6
class StaticDemo{
static {
int a=1;
System.out.println("I am a static block!");
}
}
  • 构造代码块,与静态代码块的区别在于少了 static 关键字:
constructor block demo
1
2
3
4
5
6
class ConstructorBlockDemo{
{
int a=1;
System.out.println("I am a constructor block!");
}
}
  • 构造方法
constructor demo
1
2
3
4
5
6
public class ConstructorDemo{
public ConstructorDemo(){
int a=1;
System.out.println("I am a constructor!");
}
}
  • 成员变量,一般就是类所定义的变量(描述类的属性),这个容易理解。分为默认初始化和显示初始化:
field demo
1
2
3
4
class FieldDemo{
int b;//initialized implicitly
int a=1;//initialized explicitly
}

需要注意的是,a=1;在有些情况下(比如 FieldDemo 还有父类)并不一定立即在变量 a 分配内存后被赋值。

  • 子父类的初始化,一般先进行父类初始化,然后进行子类初始化,分层进行。

执行顺序

【update】本文提到的程序可以使用在线可视化 IDE 帮助理解,Java Tutor - Visualize Java code execution to learn Java online

1. 首先程序运行时,从 main 方法所在的主类开始,但并 不意味着就是从 main 方法开始。而是 JVM 开始加载类:

LoadClass demo
1
2
3
4
5
6
7
8
9
10
public class LoadClassDemo{
static {
int a=1;
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
}
}

上述程序输出结果为(可以先自己考虑下,然后点击图片放大核对下):

JVM 在加载 LoadClassDemo 时,静态代码块就开始执行了,从而对类的信息初始化。并且静态代码块只在类加载时执行,因此只执行一次。

2. 构造代码块。静态代码块执行完毕后,构造代码块就开始执行了,而且每次生成类的实例都会执行,区别于静态代码块只执行一次。比如:

ConstructorBlock test
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
class ObjectDemo {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
public ObjectDemo() {
System.out.println("ObjectDemo constructor");
}
}
public class ConstructorBlockTest {
static {
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
ObjectDemo od1 = new ObjectDemo();
ObjectDemo od2 = new ObjectDemo();
}
}

上述程序输出结果为(可以先自己考虑下,然后点击图片放大核对下):

3. 成员变量,在创建实例时被分配内存,之后进行默认初始化和显示初始化(如有)。

4. 构造方法,上述过程之后构造方法才被调用生成对象。

TestInitialization Demo
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
class ObjectDemo {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
String f="field value";
public ObjectDemo() {
System.out.println("ObjectDemo constructor");
}
}
class ObjectDemo2 {
static {
System.out.println("ObjectDemo static block!");
}
{
System.out.println("ObjectDemo constructor block");
}
String f2="field value";
public ObjectDemo2() {
System.out.println("ObjectDemo constructor");
f2="field value changed!";
}
}
public class InitializationDemo {
static {
System.out.println("I am a static block!");
}
public static void main(String[] args) {
System.out.println("I am the main method!");
ObjectDemo od1 = new ObjectDemo();
ObjectDemo od2 = new ObjectDemo();
System.out.println(od1.f);
ObjectDemo2 od3 = new ObjectDemo2();
System.out.println(od3.f2);
}
}

仔细分析上述综合示例结果,(然后点击图片放大核对):

实例理解

实例 1

请分析一下代码执行结果,然后思考注释掉的 super 与结果又何关系

example of initialization
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
class X {
static String xVar="x value";
Y b = new Y();
static{
System.out.println(xVar);
System.out.println("X static block");
xVar="static value";
}
X() {
System.out.println("X");
System.out.println(xVar);
xVar="x value changed!";
}
}
class Y {
String yVar="Y value";
Y() {
System.out.println("Y");
System.out.println(yVar);
yVar="y value changed!";
}
void show(){
System.out.println(yVar);
}
}
public class Z extends X {
Y y ;
static{
System.out.println("Z static block");
}
{
y = new Y();
y.show();
}
Z() {
//super
System.out.println("Z");
}
public static void main(String[] args) {
System.out.println(new Z().xVar);
}
}

分析 1

先给出结果(点击图片放大):

分析过程如下(注意子父类的分层初始化):
1. 首先找到 main 方法所在类为 Z,所以最先开始加载 Z 类,由于 Z 类还有父类 X,因此又加载 X 类,先对 X 类初始化;
2.X 类加载并为静态变量 xVar 初始化,接着执行静态代码块,输出 x value,接着输出X static block 并改变 xVar 值;
3.X 类加载完之后,Z 类开始执行静态代码块输出 Z static block
4. 接着初始化 X 类,初始化成员变量 y,因此输出Yy value,由于没有构造代码块,所以继续执行构造方法输出 Xstatic value
5. 到此 X 类初始化完成,再接着执行 Z 类的构造代码块输出 YY value并改变 yVar 指,执行 show()方法后输出y value changed!,最后执行 Z 的构造方法,输出Z
6. 到此 Z 类初始化完成,最终输出x value changed![4]

实例 2

还有一个比较典型的例子[5],请分析程序执行结果:

TestInitialization Demo
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
class Base
{
Base()
{
preProcess();
}
void preProcess()
{
}
}
class Derived extends Base
{
public String whenAmISet = "set when declared";
public Derived()
{
whenAmISet = "set in constructor";
}
void preProcess()
{
whenAmISet = "set in preProcess()";
}
}
public class TestInitialization
{
public static void main(String[] args)
{
Derived d = new Derived();
System.out.println(d.whenAmISet);
}
}

分析 2

详见脚注 3,要注意本文中的例子在 Derived 类中添加了构造方法,所以最终结果为 set in constructor
更新:eclipse 调试演示图

后记

这次关于 Java 中类的初始化就介绍到这里了,关于 Java 类的生命周期可以参看 [6] 其实还有关于接口并没有提到,有兴趣可以参考 JLS 相关内容,即脚注 1 所引。另外,还是谈谈如何去深入学习一些知识的方法,比如这次类的初始化,首先当然是 search 关键字,然后自己动手写代码观察思考(还可以使用 Eclipse 断点观察程序执行流程),接着很有必要参考 JLS 中关于 Java Execution 的部分(Chapter 12),最后要想深入理解就得自己多改写代码,思考如何才能理解为何是这样执行的、这样设计有何好处。

目前就写到这里了,大家如果有改进的建议,欢迎留言~~


脚注:
[2]

一般用 new

[4]

xVar 值理应由类来访问,这里仅起到说明语法作用

文章目录
  1. 1. 初始化时机
  2. 2. 初始化步骤
    1. 2.1. 概念分析
    2. 2.2. 执行顺序
  3. 3. 实例理解
    1. 3.1. 实例 1
    2. 3.2. 分析 1
    3. 3.3. 实例 2
    4. 3.4. 分析 2
  4. 4. 后记