1.引子
不少人对于构造代码块为何先于构造方法执行感到疑惑。说到这里,还是因为对JVM的运行原理不太清楚,很多看似不可理解的行为,当你仔细了解了Java虚拟机原理之后,一切都会豁然开朗。
2.详解
很多人对于静态块先于构造方法执行比较清楚,因为静态块在类加载时就执行了,并且只执行这一次。而对于构造代码块、构造方法,谁先被执行就不太明白了。其实要弄懂一件事,构造方法(目的)不是用来创建实例对象的,而是用来初始化实例对象的。如果自己不定义构造方法,系统会调用默认的构造方法。Java源代码在使用"new"关键字时,JVM先用类加载器加载这个类,然后再经过验证(检查字节码是否符合规范)、准备(主要为静态变量分配内存空间,并在其空间内格式化充填“0”)、解析(符号引用转直接引用)、初始化(如果存在静态变量或静态代码块,jvm按照两者在源码中的原始顺序依次进行静态变量初始化赋值或执行静态块,即调用类初始化方法句柄"<clinit>"“来初始化类)。到此类加载初始化完成,再接着,jvm会执行字节码指令"new”(不同于java语言中的“new”关键字)创建一个对象,此时的对象是空的、没有实例数据、只有对象头,然后为字段(即成员变量)分配内存空间并充“0”,再然后进行成员变量赋值或执行构造代码块(即执行"<init>"方法句柄),最后再执行构造方法。
- 下面是一段《Java虚拟机规范》中的一段关于初始化方法的叙述。
2.9 初始化方法的特殊命名
在 Java 虚拟机层面上,Java 语言中的构造函数在《Java 语言规范 (第三版)》(下文简称JLS3,§8.8)是以一个名为<init>的特殊实例初始化方法的形式出现的,<init>这个方法名称是由编译器命名的,因为它并非一个合法的 Java 方法名字,不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间,通过 Java 虚拟机的 invokespecial 指令来调用,只有在实例正在构造的时候,实例初始化方法才可以被调用访问(JLS3,§6.6)。
一个类或者接口最多可以包含不超过一个类或接口的初始化方法,类或者接口就是通过这个方法完成初始化的(§5.5)。这个方法是一个不包含参数的静态方法名为"<clinit>" 。这个名字也是由编译器命名的,因为它并非一个合法的 Java 方法名字,不可能通过程序编码的方式实现。类或接口的初始化方法由 Java 虚拟机自身隐式调用,没有任何虚拟机字节码指令可以调用这个方法,只有在类的初始化阶段中会被虚拟机自身调用。
3.代码
public class Test {
static {
System.out.println("这是一个静态代码块");
}
{
System.out.println("这是一个构造代码块");
}
public Test() {
System.out.println("这是一个无参构造方法。");
}
public Test(String clName) {
System.out.println("这是一个" + clName + "类的带参构造方法。");
}
public static void main(String[] cmds){
new Test();
new Test("Test");
}
}
- 控制台输出