对于多态,大家基本上都很熟悉,日常开发用的也挺多,一句话概括:父类引用指向子类对象
- 在集合的使用上,List mList = new ArrayList<>();
- 在类的继承时,Anim anim = new Cat();
为了弄清楚多态,我们需要引入jvm方法调用的静态和动态绑定机制的概念,
jvm静态绑定机制
Public class Utils{
private static Utils utils;
private Utils(){}
Public static Utils getInstace(){
utils = new utils();
return utils ;
}
}
class testActivity extends Activity{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(setView());
Utils.getInstace();//静态绑定就在这里
}
}
静态绑定的这句代码Utils.getInstance(),会被编译器编译成一条指令:invokstatic #13,jvm是如何处理这条指令的?
(1) 指令 #13的含义:testActivity 类常量池中第13个常量表的索引项,关于常量池详见《Class文件内容及常量池 》,这个常量表记录的是方法getInstance()信息的符号引用,包括getInstance所在的类名、方法名和返回类型,jvm首先会根据这个符号引用去找到getInstance方法所在类的全包名
(2)紧接着jvm会加载、链接、初始化Utils类
(3)然后再testActivity 类所在的方法区找到getInstance()方法的直接地址,且将这个直接地址记录到testActivity 类常量池中索引为13的常量表,这个过程称为常量池解析,以后再次调用Utils.genInstance()时,直接找到方法的字节码
(4)完成了类的常量池索引项中常量表解析后,jvm就能调用这个静态方法,去执行方法逻辑
通过上述几个步骤后,我们发现当完成常量池解析后,jvm就能确定getInstance方法具体在内存的什么位置上,事实上这个信息在编译阶段就已经在类的常量池中记录了下来,在编译阶段能够确定方法在内存什么位置的机制就叫静态绑定机制
*所有私有方法、静态方法、构造器及final修饰方法都是采用静态绑定机制。在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可 *。
jvm动态绑定机制
public class AnimObj {
private String name = "anim";
public String f1() {
Log.e("11111111_Anim_getName","Anim_getName");
return name;
}
}
//子类
public class DogObj extends AnimObj {
String name = "dog";
public String f1() {
Log.e("11111111_dog_getName","Dog_getName");
return name;
}
}
//多态的调用,
AnimObj anim = new DogObj();
anim.f1();
首先我们知道,要有多态,它的前提就是继承、重写,那么jvm是如何确保找到的方法是cat 类中而不是Anim类?有点绕口,就是说代码中类的引用是Anim,正常情况下调用的方法也是Anim类中的方法,多态是怎么做到把这个方法变成调用Cat类中的方法?在这里我们先要讲一下jvm管理的一个非常重要的数据结构-----方法表
在jvm加载的同时,会在方法区中为这个类存放很多信息(详见《Java 虚拟机体系结构 》),其中有一个数据结构叫方法表,它以数组的形式记录了当前类和所有父类的可见方法字节码在内存中的直接地址,下图是对比的两个方法表
上图中的方法表有两个特点:(1) 子类方法表中继承了父类的方法,比如Anim extends Object。 (2) 相同的方法(相同的方法签名:方法名和参数列表)在所有类的方法表中的索引相同。比如Father方法表中的f1()和Sun方法表中的f1()都位于各自方法表的第11项中。
对于上面的源代码,编译器会将多态调用的方法翻译成字节码指令
//(1) Anim a = new Cat(); //在堆内存中开辟一个Cat对象的内存空间,并将对象引用压入操作栈
//invokespecial #7 [15] //调用初始化方法初始化堆中的Cat对象
//astore_1 //弹出操作树栈的Cat对象引用压入局部变量表1中
// aload_1 //取出局部变量表1的对象引用压入操作数栈
// invokevirtual #15 调用getName方法
其中invokevirtual 的详细指令是这样的:
- invokevirtual #15中的 #15指TestActivity类中常量池第15位置的常量表索引项,这个常量表记录的是方法f1信息的符号引用(包括 类名及其所有父类、方法名和返回类型),JVM首先会根据方法的符号引用找到调用f1方法的类的全包名(全限定名)com.my_project.internal_obj.obj.AnimObj,因为这是调用f1方法的类对象anim声明为Anim类型。
- 在Anim类型的方法表中查找f1方法,如果找到,将f1在方法表中的索引项11记录到TestActivity类常量池第15位置的常量表中(也就是常量池解析),需要引起注意的是,如果f1这个方法在Anim类中没有,即使Cat方法中存在,也无法通过编译,因为当前类的类型是Anim
- 在调用invokevirtual指令前有一个aload_1指令,它会将创建在堆内存中的Cat对象引用压入操作数栈,然后invokevirtual指令会根据Cat的对象引用首先找到堆内存中的Son对象,进一步找到Cat对象所属类型的方法表,下图所示:
方法数据存放在类的方法区中,包含一个方法的具体实现的字节码二进制。方法指针直接指向这个方法在内存中的起始位置,通过方法指针就可以找到这个方法。 - 这是通过第二步解析完成的#15常量表中方法表的索引项11,可以定位到Son类型方法表中的f1方法,然后通过直接地址找到该方法字节码所在的内存空间
类对象方法的调用必须在运行过程中采用动态绑定机制。
首先,根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:
① 如果能在声明类型中匹配到方法签名完全一样(参数类型一致)的方法,那么这个方法是最合适的。
② 在第①条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。
③ 如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。
然后,根据在堆中创建对象的实际类型找到对应的方法表,从中确定具体的方法在内存中的位置。
这是我自己理解的,不知道对不对!!
在编译期间,Anim对象类型调用的f1方法,是将方法表中的索引项11是指向Anim这个对象类型的,而在运行期间,Anim对象类型在堆内存中创建的实际对象是Cat,所以jvm会根据内存中真实的对象引用重新去给f1的方法表索引项11赋值,这种通过程序运行过程动态创建对象方法表的定位方法的方式,一般称之为动态绑定机制。
最后说明,域和静态方法都是不具有多态性的,任何的域访问操作都将由编译器解析,因此不是多态的。静态方法是跟类,而并非单个对象相关联的。
参考文章:http://hxraid.iteye.com/blog/428891