前言
看过像Spring这样的开源框架源码,第一个感觉:逻辑很复杂,一层一层的跳来跳去,用一个实际例子感受一下(这是Springmvc的handleMapping在找对应的requestMapping的过程一部分,这里的展示的复杂的过程中居然只是“无意义”的中转代码,还没走到真正起作用的方法)
在跟踪源码的时候,一会儿跳到父类,一会儿跳到子类。有时候会想:这怎么可能。
为什么会产生这种疑惑。因为调用父类我们比较好理解:子类继承父类,就是为了调父类资源的。那父类调用子类呢?
下面深入分析一下这种“子类和父类的相互调用”。 (本文不讨论两个明显孤立的对象之间的调用,即便这两个对象有上下级关系)
一般理解
-
子类调用父类:
为了继承并沿用父类的资源,这个动作合情合理 -
父类调用子类:
应用:钩子函数/模版方法
猜想
不存在子类调用父类,更不存在父类调用子类
结论:都是自己一个对象之间内部的调用动作。
之所以出现调用父类的感觉,是因为没看到子类的本质,实际子类体内只是包含了父类的“基因”
子类调父类方法,调的还是自己体内的方法。
另一方面,父类调用子类也只不过是错觉:一开始的那个做动作的人一定是子类。因为祖先不可能像先知一样在五千年前给你写一封信
Demo
下面看一个刚学java时可能会见到过的一个题目:
以下代码结果是_____。
class Base {
int i;
Base() {
add(1);
System.out.println(i);
}
void print() {
System.out.println(i);
}
}
class MyBase extends Base {
MyBase() {
add(2);
}
void add(int v) {
i+=v*2;
System.out.println(i);
}
}
public class TestClu {
public static void main(String[] args) {
go(new MyBase());
}
static void go(Base b) {
b.add(8);
}
}
结果:22
这里的情况,父类构造函数里调用的就是子类的add方法,而不是父类自己的。
根据我们上面猜想,这里并不存在“父类Base”,只有子类对象MyBase。所有的调用全部在子类对象MyBase内部完成,当子类重写里父类的方法,那么就用子类自己的方法。
字节码层面
先写两个简单的对比demo
package com.yc.blog.java.inherit.demo1;
/**
* 父类
*/
public class Fa {
public void m() {
System.out.println("-----Fa---m----");
}
}
package com.yc.blog.java.inherit.demo1;
/**
* 继承父类
*/
public class Ch extends Fa {
public void m1() {
System.out.println("------Ch---m1-------");
}
}
package com.yc.blog.java.inherit.demo1;
/**
* 不继承父类
*/
public class Ch2 {
public void m1() {
System.out.println("------Ch2---m1-------");
}
}
查看并对比Ch和Ch2的字节码
javap -v Ch
javap -v Ch2
可以看到两者并没有什么本质的区别,只是一个父类是Fa,另一个是Object。内部并没有增加父类的方法。
其实也比较好理解,jvm字节码安排的这么紧密,就是为了省字节空间,如果子类把“祖宗十八代”的父类全部写到自己的字节码里clas文件里,那也太占空间,这个class文件字节码也太不优美了。
java内存层面
虽然字节码层面还是 子类和父类隔离开,那么运行时是不是new子类的时候就同时new一个对应的父类呢?
不是的,内存层面的设计和上面猜想的设计思想是一致的:只有一个子类,父类的资源只是被加到子类的内存区域里(当然,会加上标签,哪些属于父类的,哪些属于子类的)
这里就不再继续深究,推荐两篇文章大家可以参考一下:
JAVA new子类的时候是否会创建父类
java中,创建子类对象时,父类对象会也被一起创建么?
回头再看Spring源码
和我们的猜想一样,上面那个复杂的Spring调用逻辑,其实就是从左下角那个“重孙”RequestMappingHandleMapping开始调用的,更准确的说,就是“重孙”自己内心的复杂OS,和它的父类们没有关系。
结论
- 不存在子类调用父类,父类调用子类的说法,所有上下级的调用都是一个类内部方法的调用,只不过是它内部继承了父类的一些方法。
- 当一个方法被几代人重写,那么就用子类自己的,子类没有就往上找,这也就是java的“多态”规则。
- 字节码层面子类和父类还是独立分开。
- 创建子类的时候并不会同时创建父类,而是在内存层面子类把父类的资源包含到自己的内存空间里。
- 框架里常用预留方法给子类重写,作为钩子函数/模板方法。如何理解钩子函数的运作机制,就是上面所解释的。