在上一篇文章中,我们发现类继承过程中,如果父类定义的方法如果子类没有重写或者重载的话,子类由于继承关系虽然能访问这个方法,但是执行的操作是在Base类之上的,这和我们的预期不同.在Child类中通过覆盖父类方法,并添加相关处理逻辑可以解决这个问题,但仔细思考了一下之后,感觉还是有点疑惑.
为什么Child类的实例里执行没有覆盖直接继承的方法,比如clear(见下方研究代码部分),这个方法对child的成员变量并没有效果,而是对其父类操作,这和我之前理解的动态绑定的过程不一致.
相关知识
代码示例
Base代码
1 | public class Base { |
Child代码
1 | public class Child extends Base { |
调用代码
1 | public static void main(String[] args) { |
结果
—- new Child()
基类静态代码块, s: 0
子类静态代码块, s: 0
基类实例代码块, a: 0
基类构造方法, a: 1
子类实例代码块, a: 0
子类构造方法 a: 10
—- c.action()
start
child s: 10, a: 20
end
—- b.action()
start
child s: 10, a: 20
end
—- b.s: 1
—- c.s: 10
补充知识
普通代码块
在方法或语句中出现的{}就称为普通代码块。
普通代码块和一般的语句执行顺序由他们在代码中出现的次序决定–“先出现先执行”
构造代码块
直接在类中定义且没有加static关键字的代码块称为{}构造代码块。
构造代码块在创建对象时被调用,每次创建对象都会被调用,并且构造代码块的执行次序优先于类构造函数。
这个构造代码块的执行顺序不会因为方法所在位置而影响,我特意将他放在构造函数之后。
静态代码块
在java中使用static关键字声明的代码块,包括使用静态方法对静态变量赋值.
静态块用于初始化类,为类的属性初始化。每个静态代码块只会执行一次。
由于JVM在加载类时会执行静态代码块,所以静态代码块先于主方法执行。
如果类中包含多个静态代码块,那么将按照”先定义的代码先执行,后定义的代码后执行“。
注意:
- 静态代码块不能存在于任何方法体内。
- 静态代码块不能直接访问静态实例变量和实例方法,需要通过
例子
1 | public class Line { |
调用
1 | public class LineTest { |
结果
注意红箭头所指的两部分的顺序取决于它出现的顺序
#####
下面我们来解释一下背后都发生了一些什么事情,从类的加载开始。
类加载过程
在Java中,所谓类的加载是指将类的相关信息加载到内存。在Java中,类是动态加载的,当第一次使用这个类的时候才会加载,加载一个类时,会查看其父类是否已加载,如果没有,则会加载其父类。
类的信息
一个类的信息主要包括以下部分
类变量(静态变量)
类初始化代码
类方法(静态方法)
实例变量
实例初始化代码
实例方法
父类信息引用
类初始化代码包括
定义静态变量时的赋值语旬
静态初始化代码块
实例初始化代码包括
定义实例变量时的赋值语旬
实例初始化代码块
构造方法
类加载过程包括
分配内存保存类的信息
给类变量赋默认值
加载父类
设置父子关系
执行类初始化代码
注意,类初始化代码,是先执行父类的,再执行子类的。不过,父类执行时,子类静态变量的值也是有的(类变量赋默认值在执行类初始化代码之前),是默认值。对千默认值,我们之前说过,数字型变量都是0,boolean是false,char是’\u0000’,引用型变量是null。
之前我们说过,内存分为栈和堆,栈存放函数的局部变量,而堆存放动态分配的对象,还有一个内存区,存放类的信息,这个区在Java中称为方法区。
加载后,Java方法区就有了一份这个类的信息。以我们的例子来说,有3份类信息,分别是Child、
Base、Object,内存布局如下图所示。
从上图也可以看出,类信息包括这几部分,这和前面说的一致
父类信息
静态变量,在类信息里以常量的方式存在
- 实例变量定义,在类信息里以引用的方式存在
- 类初始化代码
- 实例初始化代码
- 实例方法
我们用class_init()来表示类初始化代码,用instance_init()表示实例初始化代码,实例初始化代码 包括了实例初始化代码块和构造方法。例子中只有一个构造方法,实际情况则可能有多个实例初始化方法。
本例中,类的加载大致就是在内存中形成了类似上面的布局,然后分别执行了Base和Child的类初始化代码。接下来,我们看对象创建的过程。
对象创建的过程
在类加载之后,new Child()就是创建Child对象,创建对象过程包括:
分配内存
对所有实例变赋默认值
执行实例初始化代码
分配的内存包括本类和所有父类的实例变量,但不包括任何静态变量。实例初始化代码的执行从父类开始,再执行子类的。但在任何类执行初始化代码之前,所有实例变量都已设置完默认值。
每个对象除了保存类的实例变量之外,还保存着实际类信息的引用。
Child c=new Child();会将新创建的Child对象引用赋给变量c,而Base b=c;会让b也引用这个Child
对象。创建和赋值后,内存布局如下图所示。
引用型变量c和b分配在栈中,它们指向相同的堆中的Child对象。Child对象存储着方法区中Child类型的地址,还有Base中的实例变量a和Child中的实例变量a。创建了对象,接下来,来看方法调用的过程。
方法调用的过程
我们先来看c.action();,这旬代码的执行过程:
查看c的对象类型,找到Child类型,在Child类型中找action方法,发现没有,到父类中寻找;
在父类Base中找到了方法action,开始执行action方法;
action先输出了start,然后发现需要调用step()方法,就从Child类型开始寻找step()方法;
在Child类型中找到了step()方法,执行Child中的step()方法,执行完后返回action方法;
继续执行action方法,输出end。
寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候,再查找父类类型信息。
我们来看b.action(),这旬代码的输出和c.action()是一样的,这称为动态绑定,而动态绑定实现的机制就是根据对象的实际类型查找要执行的方法,子类型中找不到的时候再查找父类。这里,因为b和c 指向相同的对象,所以执行结果是一样的。
这里注意动态绑定过程,方法的查找顺序是自底向上,由于Child类没有重写action方法,c.action();调用的是Base的action
1 | public void action() { |
可以看到action调用了step方法,由于Child类重写了step方法,实例c找到的step方法就是Child类的step方法.这和上一篇里的clear方法的查找过程是一致的,但这里又有一个新的值得注意的地方,clear例子中clear修改的是父类实例变量,而这里的step操作的是子类变量.究其原因,和以下内容有关.
虚方法表
如果继承的层次比较深,要调用的方法位千比较上层的父类,则调用的效率是比较低的,因为每次调用都要进行很多次查找。大多数系统使用一种称为虚方法表的方法来优化调用的效率。
所谓虚方法表,就是在类加载的时候为每个类创建一个表,记录该类的对象所有动态绑定的方法 (包括父类的方法)及其地址,但一个方法只有一条记录,子类重写了父类方法后只会保留子类的。对于本例来说,Child和Base的虚方法表如图下所示。
Child类的实例c在调用action时,Child没有action方法,故向上查父类,在直接父类Base中找到了,Base中的action调用了step,实例c继续查找step,Child类里有step方法,于是调用的是Child的step方法.
虚方法表的设计理念与操作系统里的缓存有类似之处,不过缓存是储存最近一段时间里最常访问的记录,会动态变化,而虚方法表是固定的,这么来说虚方法表更像数据库里的索引,不过索引到最后可能也有个顺序查找的过程.
研究代码
以下是个人的理解,不正之处欢迎指出
Base类
1 | public class Base { |
Child类
1 | public class Child extends Base { |
我们修改Child在执行以下代码的过程中,在debug窗口里我发现了以前没有注意的细节.
1 | Child c = new Child(); |
以下是构造方法,child的count和arr并没有赋值语句
1 | public Child() { |
调试
执行c.clear();
之前
之后
可以看到Base.arr里的0,1,2位置的值都变回0了
一个疑问
在这个clear的例子中,调用父类的clear操作的是父类的变量,这是因为clear是属于父类Base的方法,为什么这个方法里的arr不会因为动态绑定而实际指向c的arr呢
让我们回顾这个图,类信息里记载了类实例方法,这里Base的action对应我们例子里的clear,方法的可见范围是在该类中的,和我们用子类实例取变量不同,也就是说Base里的action找到的arr是base的arr,而动态绑定发生在用c去找arr,也就是c.arr.
验证
修改后的Base
1 | import java.util.Arrays; |
修改后的Child
1 | import java.util.Arrays; |
结果
仔细观察这个部分
可以看到在Base的clear里直接使用arr的话是Base的arr即-1533386817
在Base使用getArr()由于方法的调用过程,动态绑定到子类的getArr(),于是使用的是子类Child的arr,即1493912449
几点值得注意的地方
- Child类型的实例c里不光有自己的成员变量还包括父类Base的变量,就是上图中的Base.arr和Base.count,以前没注意过(;′⌒`)
- 子类构造方法调用了父类构造方法而不做其他动作时,只会由父类构造方法操作父类的成员变量,而子类成员变量只会被赋默认值,所以子类构造方法在调用父类构造方法的同时,还要编写逻辑对自身的成员变量赋值,也就是反注释child构造方法中被注释的部分
- 在Child的构造方法中,我们print了this和super的class和hashcode信息,可以在输出中看到,它们两个是同一个类型(Child类)并指向堆里同一个东西,这和之前讲的对象创建过程是一致的.这也是我之前忽略的一点(;′⌒`)
引用资料
- Java普通代码块,构造代码块,静态代码块区别,执行顺序的代码实例
- <<Java编程的逻辑>>