前言
在juejin.cn/post/694834… 这篇文章最后我们讲了 new dup invokespecial 要形成条件反射是一个类的初始化,那篇文章我们就从字节码指令看下类的初始化。
一、简单的例子看类初始化
首先我们明确以下指令的含义:
new : 创建类的实例,分配内存空间,但是还没有调用init 构造方法
dup: 复制一份实例到操作数栈顶
Invokespecial: 调用构造器函数,在调用构造函数之前会调用 cinit (静态代码块的初始化)
//java
public class ClassInit {
static int a;
static int b;
static {
a = 1;
b = 2;
}
}
//字节码:
public class javaplan.ClassInit {
static int a;
static int b;
//默认构造函数
public javaplan.ClassInit();
Code:
0: aload_0 //加载局部变量表第一个到操作数栈顶 即this
1: invokespecial #1 // Method java/lang/Object."<init>":()V //执行构造函数
4: return
//静态代码块 对应 cInit 即类的初始化
static {};
Code:
0: iconst_1 //将int 1 加载到操作数栈顶
1: putstatic #2 // Field a:I //给变量a 赋值
4: iconst_2 //将int 2 加载到操作数栈顶
5: putstatic #3 // Field b:I 给变量b 赋值
8: return
}
我们看到 类的构造函数对应的了 init,静态代码块对应了 cinit
二、看一道老题
public class ClassA {
static {
System.out.println("A init");
}
public ClassA() {
System.out.println("A Instance");
}
}
public class ClassB extends ClassA{
static {
System.out.println("B init");
}
public ClassB () {
System.out.println("B Instance");
}
public static void main(String[] args) {
ClassA a = new ClassB();
//ClassB [] arr = new ClassB[10];
}
}
//C
public class ClassInit {
public static void main(String[] args) {
ClassB[] array = new ClassB[10];
}
}
(1).ClassB中 调用了 new ClassB(),会输出什么?
(2). main方法中 换成 ClassB [] arr = new ClassB[10] 会输出什么?
(3).新的类 ClassInit 中 调用ClassB[] array = new ClassB[10] 会输出什么?
a.我们先看第一种情况的字节码:
public class javaplan.ClassA {
//A 的构造函数 即 A的init
public javaplan.ClassA();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String A Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
//A 的cinit 即A的 类初始化,静态代码块
static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String A init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
public class javaplan.ClassB extends javaplan.ClassA {
//B的构造方法 即B init
public javaplan.ClassB();
Code:
0: aload_0
1: invokespecial #1 // Method javaplan/ClassA."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String B Instance
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
//B 的静态main 方法
public static void main(java.lang.String[]);
Code:
// 执行new ClassB
0: new #5 // class javaplan/ClassB
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: return
//B 的类初始化 cinit 即B的静态代码块
static {};
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String B init
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
可以看到B的构造方法(init)中 抵用了 A类的构造方法(init),而 调用构造方法前会调用类的 cinit(静态代码块),new ClassB(),会触发 B init,而B init 会先调用 B cinit, Binit 会先调用 A init,而A的init会先调用 A cinit,所有顺序是 B cinit 、 A cinit、 A init 、B init
b. 我们看下第二种情况:
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #5 // class javaplan/ClassB
5: astore_1
6: return
第二种情况只是new 了一个 B的数组,因为main 方法是 B类的 静态方法,只涉及到B类的加载,并不涉及到 ClassB 的init 方法调用,所以也不会调用 A类的cinit 和 init,所有输出结果只有 B cinit
c. 第三中情况 我们看下 classinit 的字节码:
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: anewarray #2 // class javaplan/ClassB
5: astore_1
6: return
由于main 方法属于 ClassInit,new 一个数组只涉及分配内存,并不会触发 B的初始化,所以什么也不会输出。
三、看一个问题
public class ClassInit {
int a;
public void main(String[] args) {
int b;
System.out.println(a);
System.out.println(b);
}
}
如上述代码,编译器告诉我们 b 未初始化,会报错,那么为什么a 不会报错呢?
这就需要我们了解类的加载过程
- 类加载校验
- 执行 static 代码块 即 cinit
- 为对象分配堆内存
- 对成员变量进行初始化(对象的实例字段在可以不赋初始值就直接使用,而局部变量中如果不赋值就直接使用,因为没有这一步操作,不赋值是属于未定义的状态,编译器会直接报错)
- 调用初始化代码块 {} 内的代码
- 调用构造器函数 init
有了上述流程,也就是 成员变量的初始化在 构造函数之前,但局部变量如果不赋值,就属于未定义状态,则不可用。
四、总结
New 一个类的执行顺序:静态代码块(cinit)、初始化代码块({}内的代码)、构造函数(init)