0%

Java 代码执行时机

前言

简单了解一下 Java 代码中,成语变量、静态变量、代码块、构造函数之类的内容的执行顺序。

从一个类的用途出发,他会经历 类的加载、类的初始化、类的实例初始化 这三个阶段。

其中加载更多的虚拟机内部的细节,这里重点说一下类的初始化、类的实例化相关的内容。

类的加载

Java 做为一种解释执行的语言,我们编写的 java 代码通过 javac 命令编译之后,会被编译成相应的 class 文件。然后 Java 虚拟机会加载并执行这些文件。关于加载和执行的具体细节,按照《深入了解 Java 虚拟机》中的介绍,会按如下步骤进行。

class 文件的加载

从上图我们已经可以了解到一个 .class 文件到一个真实的 Java 对象之前会经历哪些步骤。这当中装载、链接、init 方法的执行等都是虚拟机细节相关的部分,这里我们重点看一下 类中各个成员变量的初始化。具体的执行细节阅读 类的初始化深入探索

类的初始化和实例化

在学习 Java 继承这一特性的时候,我们就知道在执行子类构造函数的时候会优先执行父类的构造函数,并依次向上传递。这是因为在继承这个特性上,子类需要依赖父类相关属性的初始化。

但是当类中包含静态变量、静态常量、静态代码块、代码块时又会是一种什么样的执行逻辑呢?我们可以简单试一下。

类的初始化

先看两个类 Person.java 和 Student.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
public class Person {
// 静态变量
public static int value1 = printAndReturn(100);
// 静态常量
public static final int value2 = printAndReturn(200);

// 普通变量
public int value4 = 400;

// 静态代码块
static {
value1 = 101;
System.out.println("Person static block");
}

// 代码块
{
value1 = 102;
System.out.println("Person block");
}

// 构造函数
public Person() {
value1 = 103;
System.out.println("Person constructor");
}

// helper method
public static int printAndReturn(int param) {
System.out.println("Person:" + param);
return param;
}
}

这里 printAndReturn() 方法的主要作用是确定静态成员的执行顺序,因为变量或常量的初始化本身不会有输出,因此这里通过中间赋值的方式,确认其执行顺序

  • 子类
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
public class Student extends Person {
public static int value3 = printAndReturn(300);

public int value5 = 500;

static {
value3 = 301;
System.out.println("Student static block");
}

{
value3 = 302;
System.out.println("Student block");
}

public Student() {
value3 = 303;
System.out.println("Student constructor");
System.out.println("--------------------");
}

public static int printAndReturn(int param) {
System.out.println("Student:" + param);
return param;
}
}

要实现类的加载,注意这里是加载,不是初始化。有两种方式,

  • Class.forName(“类的全量限定名”)

  • Class.staticField

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
         public static void main(String[] args) {

    int temp = Student.value3; // logic 1

    try {
    // Class clazz = Class.forName("com.basic.classloads.Student"); // logic 2
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    output

    1
    2
    3
    4
    5
    Person:100
    Person:200
    Person static block
    Student:300
    Student static block

上面的 main 方法,无论是单独执行 logic 1 还是 logic 2。 结果都是相同的输出。这可以看到,在没有创建主动创建类的实例的时候,当我们用到一个类的时候,只会执行初始化其静态成员,执行静态代码块。构造函数之类的是不会执行的。

这里可以看到静态成员是按照其在代码中声明的顺序执行。Person 类按照 value1 ,value2 ,静态代码块的顺序一次执行。
这里大家可以调整代码顺序自己体会一下,就不再验证了。

实例初始化

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {

Student s1 = new Student();
Student s2 = new Student();// logic 1

try {
Class clazz = Class.forName("com.basic.classloads.Student"); // logic 2
clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}

这里我们通过不同的方式,创建了 3 个 Student 的实例。我们再来看看输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Person:100
Person:200
Person static block
Student:300
Student static block
Person block
Person constructor
Student block
Student constructor
--------------------
Person block
Person constructor
Student block
Student constructor
--------------------
Person block
Person constructor
Student block
Student constructor
--------------------

从结果我们可以得出执行顺序:

  1. 父类静态变量和静态代码块;

  2. 子类静态变量和静态代码块;

  3. 父类普通成员变量和普通代码块;

  4. 父类的构造函数;

  5. 子类普通成员变量和普通代码块;

  6. 子类的构造函数。

同时也可以看到,静态的内容,是属于类的,和单个的实例无关,因此只会执行一次。

潜在的坑

按照上面的逻辑,似乎类的实例化一定是在类的初始化完毕只会执行,其实不然。

1
2
3
4
5
6
class A {
public static A a = new A();


public A() {}
}

在这样的代码中,由于静态成员的初始化使用了当前类的构造函数,那么就会在这个过程中发生实例变量的初始化。

参考文档

加个鸡腿呗.