关于动态绑定的实现机制
多态是通过动态绑定实现的。那么动态绑定是如何实现的呢?虚拟机是如何找到正确的方法呢?先看下面代码:
class Father{
public void f1(){
System.out.println("father-f1()");
}
public void f1(int i){
System.out.println("father-f1() para-int "+i);
}
}
//被调用的子类
class Son extends Father{
public void f1(){ //覆盖父类的方法
System.out.println("Son-f1()");
}
public void f1(char c){
System.out.println("Son-s1() para-char "+c);
}
}
//调用方法
import hr.test.*;
public class AutoCall{
public static void main(String[] args){
Father father=new Son(); //多态
father.f1(); //打印结果: Son-f1()
}
}
打印结果为执行Son中的f1()方法。
那么JVM是如何知道f1()调用的是子类中的而不是父类中的呢?
这是因为虚拟机实现的动态绑定机制,如果说一种语言想实现动态绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是,方法调用机制能找到正确的方法体,并加以调用,不管怎样必须在对象中安置某种“类型信息”。
那么对象在内存中是怎么样的呢?
一个对象在内存中由对象头和实例数据以及对其填充组成。对象头主要包括对象自身的运行行元数据,比如哈希码、GC分代年龄、锁状态标志等;同时还包含一个类型指针,指向类元数据,表明该对象所属的类型。实例数据它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)。
用图简单表示如下:
通过这个指向方法表的指针,我们可以找到方法区中该类的信息。
那么方法表又是什么东西呢?
它是以数组的形式记录了当前类以及所有超类的可见方法字节码在内存中的直接地址,在方法表中,来自超类的方法出现在子类的方法之前,并且排列方法指针的顺序和方法在Class文件中出现的顺序相同。方法表如下图所示:
在这个例子中,首先Fatherfater =new Son();构造一个对象,base指向堆中的对象,然后根据对象头中的类型指针找到方法区中该对象所属类型,现在虚拟机知道了该对象是Father还是Son类,同时方法区中存在着方法表。当执行father.f1()时,就在该实际类的方法表中根据方法签名(这个方法签名存在于常量池中)搜索适合的方法,然后调用,如果在子类中没有找到合适的方法,则向上转型到父类Father表中的方法表中搜索。以此类推。
还有一种情况需要我们注意,代码如下:
class Father{
public void f1(){
System.out.println("father-f1()");
}
public void f1(int i){
System.out.println("father-f1() para-int "+i);
}
}
//被调用的子类
class Son extends Father{
public void f1(){ //覆盖父类的方法
System.out.println("Son-f1()");
}
public void f1(char c){
System.out.println("Son-s1() para-char "+c);
}
}
//调用方法
import hr.test.*;
public class AutoCall{
public static void main(String[] args){
Father father=new Son();
char c='a';
father.f1(c); //打印结果:father-f1() para-int 97
}
}
为什么会执行父类中的方法呢?
因为父类的方法在子类方法之前,简单表示如下图:
当搜索的时候,根据顺序找到f1(int),JVM通过参数自动转型char--->int,f1(char)匹配f1(int),所以执行了父类中的f1(int)。
还有一种情况,就是如果通过自动转型发现可以“凑合”出两个方法怎么办?
如下代码:
class Father{
public void f1(Object o){
System.out.println("Object");
}
public void f1(double[] d){
System.out.println("double[]");
}
}
public class Demo{
public static void main(String[] args) {
new Father().f1(null); //打印结果: double[]
}
}
遇到这种情况,有一个很重要的标准就是:如果一个方法可以接受传递给另一个方法的任何参数,那么第一个方法就相对不适合。比如上面代码:任何传递给f1(double[])方法的参数都可以传递给f1(Object)方法,而反之却不行,那么f1(double[])方法就更合适,因此JVM就会调用这个更合适的方法。
以上,这时动态绑定的一种实现方式,根据不同的Java虚拟机平台和不同的实际约束,动态绑定可以有不同的内部实现机制。