Java多态的实现原理

多态:父类的引用指向子类对象

继承是多态实现的基础,要是没有继承,多态也就无从谈起了。

那么JVM到底是怎样实现多态的呢?

public class Test {
     public void perform(Latin dance){
         dance.play();
     }
     public void perform(Jazz dance){
         dance.play();
     }
      public static void main(String[] args){
         new Test().perform(new Latin()); // Upcasting
     }
}

为了解决这个问题,Java使用了后期绑定的概念,当向对象发送消息时,在编译阶段,编译器只保证被调用的方法的存在,并对调用参数和返回类型进行检查,但是并不知道将被执行的确切代码,被调用的代码直到运行时才能确定。上面的代码为例,,Java在执行后期绑定时,JVM会从方法去区中的Latin方法表中取到Latin对象的直接地址,这就是为什么调用的是Latin.play的原因。

将一个方法调用同一个方法主体关联起来被称作绑定,Java中分为前期绑定和后期绑定(动态绑定或运行时绑定),在程序执行之前进行绑定,(由编译器和连接程序实现)叫做前期绑定,因为在编译阶段被调用方法的直接地址就已经存储在方法所属类的常量池中了,程序执行时直接调用。后期绑定含义就是在程序运行时根据对象的类型进行绑定,想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而找到对应的方法,简而言之就是必须在对象中安置某种“类型信”,Java中除了static方法、final方法(private方法属于)之外,其他的方法都是后期绑定。后期绑定会涉及到JVM管理下的一个重要数据结构——方法表,方法表以数组的形式记录当前类及其所有父类的可见方法字节码在内存中的直接地址。

动态绑定的具体调用过程为:

1. 首先找到被调用方法所属类的全限定名

2. 在此类的方法表中寻找被调用方法,如果找到,会将方法表中此方法的索引项记录到常量池中(此过程叫常量池解析),如果没有,编译失败。

3. 根据具体实例化的对象知道方法区中此对象的方法表,再找到方法表中的被调用方法,最后通过直接地址找到字节码所在的内存空间。

域和静态方法都是不具有多态性的,任何的域访问操作都将由编译器解析。因此不是多态的,静态方法是跟类,而并非单个对象相关联的。

————————————————————————————————————————————————————————

静态绑定机制 

//被调用的类  
package hr.test;  
class Father{  
      public static void f1(){  
              System.out.println("Father— f1()");  
      }  
}  
//调用静态方法  
import hr.test.Father;  
public class StaticCall{  
       public static void main(){  
            Father.f1(); //调用静态方法  
       }  
}  

上面的源代码中执行的方法调用的语句(Father.f1())被编译器编译成一条指令:invokestatic #13,我们看看JVM是如何处理这条指令的:

(1) 指令中的#13指的是StaticCall类的常量池中第13个常量表的索引项。这个常量表记录的是方法f1信息的符号引用(包括f1所在的类名、方法名、返回类型)。JVM会首先根据这个符号引用找到方法f1所在的类的全限定名:hr.test.Father。

(2) 紧接着JVM会加载、链接和初始化Father类。

(3) 然后再Father类所在的方法区中找到f1()方法所在的直接地址,并将这个直接地址记录到StaticCall类的常量池索引为13的常量表中。这个过程叫常量池解析,以后再次调用Father.f1()时,将直接找到f1方法的字节码。

(4) 完成了StaticCall常量池索引项13的常量表解析之后,JVM就可以调用f1方法,并开始执行f1方法中的指令了。

通过上面的过程,我们发现经过常量池解析之后,JVM能够确定要调用的f1方法具体在内存中的什么位置上。实际上,这个信息在编译阶段就已经在StaticCall类的常量池中记录了下来,这种在编译阶段就能够确定的调用哪个方法的方式,叫做静态绑定机制。

被static修饰的静态方法会被编译成invokesatic指令,另外调用私有方法、实例构造器<init>方法和父类方法都会被编译成invokespecial指令。JVM会采用静态绑定机制来顺利的调用这些方法。

动态绑定机制

package hr.test;  
//被调用的父类  
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()  
    }  
}  

上面的源代码中有三个重要的概念:多态(polymorphism) 方法覆盖 、方法重载 。打印的结果大家也都比较清楚,但是JVM是如何知道f.f1()调用的是子类Sun中方法而不是Father中的方法呢?在解释这个问题之前,我们首先简单的讲下JVM管理的一个非常重要的数据结构——方法表 

在JVM加载类时,会在方法区中为这个类存放很多信息。其中一个数据结构叫方法表。它以数组的形式记录了当前类及其所有超类的可见方法字节码在内存中的直接地址。下图是上面源代码中Father和Sun类在方法区中的方法表:

上图中的方法表有两个特点:(1) 子类方法表中继承了父类的方法,比如Father extends Object。 (2) 相同的方法(相同的方法签名:方法名和参数列表)在所有类的方法表中的索引相同。比如Father方法表中的f1()和Son方法表中的f1()都位于各自方法表的第11项中。

对于上面的源代码,编译器首先会把main方法编译成下面的字节码指令:

0  new hr.test.Son [13] //在堆中开辟一个Son对象的内存空间,并将对象引用压入操作数栈  
3  dup    
4  invokespecial #7 [15] // 调用初始化方法来初始化堆中的Son对象   
7  astore_1 //弹出操作数栈的Son对象引用压入局部变量1中  
8  aload_1 //取出局部变量1中的对象引用压入操作数栈  
9  invokevirtual #15 //调用f1()方法  
12  return  

 其中invokevirtual指令的详细调用过程是这样的:

(1) invokevirtual指令中的#15指的是AutoCall类的常量池中第15个常量表的索引项。这个常量表(CONSTATN_Methodref_info ) 记录的是方法f1信息的符号引用(包括f1所在的类名,方法名和返回类型)。JVM会首先根据这个符号引用找到调用方法f1的类的全限定名: hr.test.Father。这是因为调用方法f1的类的对象father声明为Father类型。

(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项11(如上图)记录到AutoCall类的常量池中第15个常量表中(常量池解析 )。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。

(3) 在调用invokevirtual指令前有一个aload_1指令,它会将开始创建在堆中的Son对象的引用压入操作数栈。然后invokevirtual指令会根据这个Son对象的引用首先找到堆中的Son对象,然后进一步找到Son对象所属类型的方法表。过程如下图所示:

(4) 这是通过第(2)步中解析完成的#15常量表中的方法表的索引项11,可以定位到Son类型方法表中的方法f1(),然后通过直接地址找到该方法字节码所在的内存空间。

很明显,根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。这种在程序运行过程中,通过动态创建的对象的方法表来定位方法的方式,我们叫做 动态绑定机制 

参考:http://hxraid.iteye.com/blog/428891

参考:https://www.cnblogs.com/startRuning/p/5673485.html

猜你喜欢

转载自blog.csdn.net/weixin_42294335/article/details/81205936