从字节码层面看父子类的执行顺序
起因
我相信很多人都遇到过这种题目,一个父子类,然后我们new 子类,让我们说出打印的顺序。我刚学的时候,只能死记硬背类的加载顺序。类似这种,父类静态代码块>子类静态代码块>当前主程序>父类非静态代码块>父类构造函数>子类非静态代码块>子类构造方法>子类一般方法。但是我在学了jvm以后,我就在想,如何从字节码层面上解释这个执行顺序。
例子
https://blog.csdn.net/qq_42449963/article/details/103372464这个博客算讲得不错的,但是别人讲的终究是别人讲的,我自己也要讲一遍。
class A {
public static String a1 = "静态属性";
String a2 = "实例变量";
static {
System.out.println("A---》" + a1);
System.out.println("A---》静态代码块");
}
{
System.out.println("A---》" + a2);
System.out.println("A---》构造代码块");
}
public A() {
System.out.println("A---》构造方法");
}
}
class B extends A {
public static String b1 = "静态属性";
String b2 = "实例变量";
static {
System.out.println("B---》" + b1);
System.out.println("B---》静态代码块");
}
{
System.out.println("B---》" + b2);
System.out.println("B---》构造代码块");
}
public B() {
System.out.println("B---》构造方法");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("第一次:");
new B();
System.out.println("--------------隔离横线-----------------");
System.out.println("第二次:");
new B();
}
}
执行结果
第一次:
A---》静态属性
A---》静态代码块
B---》静态属性
B---》静态代码块
A---》实例变量
A---》构造代码块
A---》构造方法
B---》实例变量
B---》构造代码块
B---》构造方法
--------------隔离横线-----------------
第二次:
A---》实例变量
A---》构造代码块
A---》构造方法
B---》实例变量
B---》构造代码块
B---》构造方法
我看先看下执行方法,是Test类的main方法,Test没有父子类,所以加载没什么。然后看执行方法,new B()。
那么我们得去加载B类到方法区(元空间)。
加载是什么?
加载就是 加载-》链接(验证-》准备-》解析)-》初始化
加载
- 通过全限定类名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。 - 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
第二阶段,保证不存在不符合 Java 语言规范的元数据信息。 - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
- 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。
可以考虑使用 -Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。
解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 <clinit>()
方法的过程。
<clinit>()
方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并产生的。(不包括构造器中的语句。构造器是初始化对象的,类加载完成后,创建对象时候将调用的 <init>()
方法来初始化对象)
从上面可以看出,准备阶段的时候,会给类变量赋初始值,也就是B类的b1变量会变“”。这个初始值要看是什么类。然后初始化阶段执行clinit方法,进行类变量的显示赋值
以及执行static静态代码块中的内容
,这两个过程的执行顺序取决于他们的代码执行顺序
。那么B.b1会变静态属性,然后打印出来B—》静态属性,B—》静态代码块。
另外该方法执行之前需要先加载父类,由于B类的父类
是A类
,所以A类
需要先完成加载、链接、初始化之后才会轮到B类的初始化方法执行。同理会先执行
A的A—》静态属性,A—》静态代码块。
加载完毕后,才能进行new创建对象。也就是调用init方法。查看字节码,就可以看出new B()是调用B的init方法。查看B的字节码,可以看出B的init方法是调用A的init方法。我们就知道了,子类初始化会先初始化父类。那么这个初始化是什么意思呢?引出一个问题,子类初始化是初始化出一个子类对象,还是两个(子类+父类)。 其实只有一个对象,这个下文讨论。
那么这个初始化干了啥呢
<init>方法
中执行如下操作:
- 默认初始化
- 显式初始化 / 代码块中初始化(这两个的执行顺序取决于代码顺序)
- 构造器中初始化
也就是说,执行了A—》实例变量 A—》构造代码块 A—》构造方法 然后执行子类的初始化方法。
那么类的执行顺序就很清晰了。
那么怎么理解父子类的属性和方法重写呢?
public class Father {
int x =0;
public Father(){
this.print();
x=20;
}
public void print(){
System.out.println("Father.x="+x);
}
}
class Son extends Father{
int x =30;
public Son(){
this.print();
x=40;
}
public void print(){
System.out.println("Son.x="+x);
}
public static void main(String[] args) {
Father son = new Son();
son.print();
}
}
Son.x=0
Son.x=30
Son.x=40
这个从字节码怎么理解呢?我们一步步来,我们的方法在son类中,那么就是加载father类son类。这个没有静态类,那么没用。然后是执行,new son()。也就是执行son的init方法,而执行son的init方法之前会执行Father的init方法,也就是
public Father(){
this.print();
x=20;
}
这里问题也就来了,这个this指向的是哪个对象,也就是说,子类的实例化,会实例化父类对象吗? 不会
https://blog.csdn.net/z56zzzz/article/details/78086434
https://www.jianshu.com/p/4b6379415e3a
调用的super(),只是为了给继承父类而来的属性初始化
很容易认为java中既然调用了父类的构造方法,那么也就创建了当前子类的父类对象. 此种观点是由理解的误区的。
在创建子类对象时,会把父类里的成员变量和方法也加载进内存(因为要加载进内存,所以要看下这些数据是怎么初始化的,所以调用了父类的构造,仅此而已,并不是去创建了父类对象),然后用this和super这两个引用来区分是父类的还是子类的,但是这个内存区域是子类的内存区域,绝不是父类的 this指向了不仅父类可继承的成员变量和可继承的方法外,它还指向了子类的成员变量和方法 而super仅仅只是指向了子类对象中从父类继承的成员变量和方法。
那么我们可以知道,this和surper其实都是执行子类对象,不过super可以执行从父类继承的方法,
那么
public Father(){
this.print();
x=20;
}
这个print其实指向的是子类的重写的print方法,那么此时son的x并没有构造器显示初始化,也就是还是0。
然后会调用son的init方法。此时还是调用son的print方法,那么此时son的x已经被显示初始化,也就是30,那么就会打印出30。
然后son的x被赋值为40.
然后执行完构造器方法后,又调用了son.print方法。因为实际对象是son对象,所以还是调用的son的print方法,因此打印出40.
public class Father {
int x =0;
public Father(){
this.print();
x=20;
}
private void print(){
System.out.println("Father.x="+x);
}
}
class Son extends Father{
int x =30;
public Son(){
this.print();
x=40;
}
public void print(){
System.out.println("Son.x="+x);
}
public static void main(String[] args) {
Father son = new Son();
System.out.println(son.x);
}
}
Father.x=0
Son.x=30
20
那么这个时候也就知道为什么了。
在new son的时候,内存大概是{father.x=20,x=40 }
在调用father的init方法的时候,print没有被重写,可见的只能调用father的print方法。
然后son.x的属性,可见性只能看到father的x。所以是20
https://www.cnblogs.com/czwbig/p/11127222.html