标签:运行时 static代码块 类继承 指针 这一 本地线程 所有者 第一步 开启
首先我们先看一个段非常有代表性的代码,里面一口气牵扯到了多态和类初始化顺序知识。
public class Test {
public static void main(String[] args) {
A test = new B();
}
}
class A {
int value = 10;
A() {
System.out.println("父类构造器");
process();
}
public void process() {
System.out.println("父类的process");
value++;
System.out.println(value);
}
}
class B extends A {
int value = 12;
{
value++;
}
B() {
System.out.println("子类构造器");
process();
}
public void process() {
System.out.println("子类的process");
System.out.println(value);
value++;
System.out.println(value);
}
}
它的输出是:
父类构造器
子类的process
0
1
子类构造器
子类的process
13
14
我想现在你一定很困惑,不要慌上车!带你了解底层的原理
这里的底层原理是Java的动态分派机制
对于方法重写,Java采用的是动态分派机制,也就是说在运行的时候才确定调用哪个方法。由于A的实际类型是B,因此调用的就是B的process()法。
原理在底层字节码中的invokevirtual指令的多态查找过程,分为以下几个步骤:
从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为直接引用,这就是方法重写的本质。
相信到这你还是迷迷糊糊的,那是因为缺少对类加载过程中解析知识的了解
解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。
通俗点说,所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是——>方法在程序真正运行之前就有一个可确定的调用版本(主要是静态方法和私有方法),它们的调用版本在运行期是不可变的。因为静态方法和私有方法不可能通过继承或别的方式重写成其他版本!!划重点——>其他版本,因此他们都在类加载阶段解析完成了。
综上可知,在动态分派的机制下,因为子类继承父类重写了process()方法,只有在程序运行时才能确定的调用版本,将符号引用转化成了直接引用,指向了实例的process()方法。
这种在运行期根据实际类型确定方法执版本的分派过程就是动态分派。
这是因为在对象实例化的时候,划分内存后会直接赋零值。
可以知道,当父类调用子类的process()方法时,子类并没有初始化完成,仅仅是分配了内存,这里有个实例变量初始化顺序:
遵循的原则是:
(1)按照代码中的顺序依次执行实例变量定义语句和实例变量代码块;
(2)如果创建该类的对象时该类的类变量尚未初始化,则先初始化类变量,再初始化实例变量;
(3)如果该类有父类的话,则先创建一个父类对象;并且,如果父类类变量没被初始化时,先初始化父类的类变量,再初始化父类的实例变量,再调用父类的默认构造器;
//有继承的情形(且该类和父类的类变量未被初始化)
1.父类的static变量初始化和static代码块
2.子类的static变量初始化和static代码块
3.父类的实例变量初始化和实例变量初始化代码块
4.父类的构造函数
5.子类的实例变量初始化和实例变量初始化代码块
6.子类构造函数
相信到这你理解了为什么会打出0和1了,是因为父类的构造函数是在子类的实例变量初始化之前执行的。所以当输出value时,其值为0。
标签:运行时 static代码块 类继承 指针 这一 本地线程 所有者 第一步 开启
原文地址:https://www.cnblogs.com/keeya/p/9380107.html