- 博主简介:努力学习的预备程序媛一枚~
- 博主主页: @是瑶瑶子啦
- 所属专栏: Java岛冒险记【从小白到大佬之路】
学习了继承、多态
本节,将通过一个简单的例子,从概念上介绍原理(实际实现的细节与此有所差别),更好的清晰明了的掌握继承!
目录
- Part1:背景:
- Part2:类加载流程
- 2.1:类包含的信息
- 2.2:类加载流程
- 2.3:内存布局
- Part3:对象创建过程
- 3.1:内存分布
- Part4:实例方法调用
- Part5:访问属性
Part1:背景:
这里我们用两个类来演示一下:
public class Father {
public static int count;
private int a;
static {
System.out.println("Father类的静态初始化代码块被执行");
count = 1;
}
{
System.out.println("Father类的实例代码块被执行");
a = 1;
}
public Father() {
System.out.println("Father类的构造器被调用");
a = 2;
}
protected void enjoy() {
System.out.println("enjoy smoking");
}
public void action() {
System.out.println("Start");
enjoy();
System.out.println("Ended");
}
}
public class Child extends Father {
public static int count;
private int a;
static {
System.out.println("Child类的静态代码块被调用");
}
{
System.out.println("Child类的实例代码块被调用");
}
public Child() {
System.out.println("Child类的构造方法被调用");
}
@Override
protected void enjoy() {
System.out.println("enjoy studying");
}
}
当我们在测试类中创建子类Child c = new Child()
,由于类第一次被使用,类会被加载进内存。那类是怎么样被加载进内存的呢,又把哪些信息加载进了内存哪里?这之间发生了什么,我们接下来将进行系统的讲解。
Part2:类加载流程
2.1:类包含的信息
在类第一次被使用的时候,类会被JVM加载进内存中的方法区。方法区中存储着类的信息,类的信息包括哪些呢?
2.2:类加载流程
2.3:内存布局
知道了类在何时加载进内存、加载进内存的哪个位置、将哪些信息存储、加载流程之后,我们来看看此时方法区的内存分别是怎样的。
就我们的例子来说,完整加载流程走完之后,内存会保存三个类的信息,分别是:顶级父类Object、Father、Child
Part3:对象创建过程
在new 对象
时,首先第一步是将相应的类及其父类加载进内存,并完成其初始化,第一步在Part2已经讲解完,所有类加载并初始化后内存布局已经给出。我们现在来看一下Child c = new Child()
JVM所要做的第二步:在堆内存中创建对象的过程。
3.1:内存分布
Part4:实例方法调用
关于实例方法调用,实例方法重写和动态绑定,已经在这两篇文章中详细叙述
但是这里就基本原理的角度,再次讲解一遍,使印象深刻
再次强调:静态属性、静态方法和非静态的属性都可以被继承和隐藏(hide),而不能够被重写!也更谈不上动态绑定。
Child c = new Child();
f = c;
c.action();
f.action();
首先,在调用方法前,JVM内存分布如下:
-
c.action()
- 首先,通过c找到堆内存中对象,发现实际类型信息Child
- 到Child类型信息中寻找实例方法action()–>没有找到
- 通过Child类型信息中父类信息引用,向上寻找父类类型信息
- 发现父类中存在相匹配的实例方法action(),并调用。
- 执行第一条语句 ,
System.out.println("Start");
输出“Start" - 执行第二条语句,调用实例方法
enjoy()
;到实际类型Child中寻找enjoy方法,找到了并且调用 - 执行第三条语句:
System.out,println("End");
- 执行第一条语句 ,
-
f.action()
(节约时间的话,可以不用看了,和c,action()
一模一样)- 首先,通过f找到所指向的在堆内存中的对象,实际类型信息为Child
- 到Child类型信息中寻找实例方法action()–>没有找到
- 通过Child类型信息中父类信息引用,向上寻找父类类型信息
- 发现父类中存在相匹配的实例方法action(),并调用。
- 执行第一条语句 ,
System.out.println("Start");
输出“Start" - 执行第二条语句,调用实例方法
enjoy()
;到实际类型Child中寻找enjoy方法,找到了并且调用 - 执行第三条语句:
System.out,println("End");
- 执行第一条语句 ,
【总结】
- 引用在调用对象实例方法时,会找到在堆内存中对象的实际地址(保存了该对象的实际类型信息的引用)
- 然后根据实际类型信息调用实例方法
- 如果在此类中找不到对应实例方法,将会从实际类型开始,逐级向上(父类)中查找,直到调用到合适实例方法。
这里f.action()
和c.action()
所执行的结果完全相同,为什么?因为f和c指向的是堆内存中同一对象,同一对象的实际类型唯一。而调用对象实例方法看的就是实际类型,所以自然方法调用所指向的结果相同!
这其实就是动态绑定的实现机制:根据对象的实际类型调用实例方法,在实际类型中找不到,就逐级向上(父类)中查找。
【补充】:虚方法表
根据上述讲解,到现在,我们堆方法重写,动态绑定已经非常清楚。
我们看到,在判断调用实例方法时,要做一个操作:向上查找,直到找到。如果继承只有一两层还会,如果继承层次太深,每一次都要进行这种查找,效率比较低。于是为了优化,提出了虚方法表的概念。
📍所谓虚方法表,就是在每个类在创建的时候,为其创建一个表,来记录该类对象所有动态绑定方法(包括从父类继承过来的方法)及其地址。一个方法只有一条,如果该类重写了从父类继承过来的方法,那么该方法记录的就是子类重写之后的那个方法。
所以,在本篇文章的背景下,虚方法表是这样的:
有了虚方法表,只要我们确定了该对象的实际类型,就可以通过查该类型的虚方法表的方式来直接确定调用哪个实例方法。效率就会提高很多。
Part5:访问属性
我们知道,只有方法才谈动态绑定,属性是不存在什么动态绑定的。一下理解都是正确的:
- 属性的访问看编译类型
- 访问属性是静态绑定,无论是否为静态!
访问过程:
- 首先,由于静态绑定的存在,查看堆内存中该编译类型对应的实例变量(该对象的堆内存中存在父类的实例变量和本类的实例变量)
- 若存在,则直接访问
- 本编译类型不存在,则从本编译类型开始逐级向上查找,直到某个父类中存在同名属性且可以访问,则访问。(查找关系)
-
Java岛冒险记【从小白到大佬之路】
-
LeetCode每日一题–进击大厂
-
Go语言核心编程
-
算法