Java动态绑定和静态绑定-多态

一、问题
Java方法调用过程中,Jvm是如何知道调用的是哪个类的方法?Jvm又是如何处理?
 
二、概念
a、当子类和父类(接口和实现类)存在同一个方法时,子类重写父类(接口)方法时,程序在运行时调用的方法时,是调用父类(接口)的方法呢?还是调用子类的方法呢?我们将确定这种调用何种方法的操作称之为绑定。
 绑定又分为静态绑定和动态绑定。
 
静态绑定
静态绑定是在程序执行前就已经被绑定了(也就是在程序编译过程中就已经知道这个方法是哪个类中的方法)。
public class StaticBindDemo {
 
public static void s1() {
System.out.println("static s1");
}
 
private void p1() {
System.out.println("private p1");
}
 
public final void f1() {
System.out.println("final f1");
}
}

  

 
调用方:
public class StaticCall {
public static void main(String[] args) {
StaticBindDemo sbd = new StaticBindDemo();
StaticBindDemo.s1();
sbd.f1();
}
}

  

 
反编译后的文件:
 
 
上面的源代码反编译后,我们可以看到
调用的是静态方法
8: invokestatic #4 // Method com/jstar/jvm/sync/bind/StaticBindDemo.s1:()V
 
1、#4指的是常量沲中的第4个常量表索引项,记录的是方法s1的符号引用,jvm会根据这个符号引用找到方法f1
所在的类的全限定名: com/jstar/jvm/sync/bind/StaticBindDemo
2、紧接着JVM会加载、边接和初始化类StaticBindDemo类
3、然后在StaticBindDem类所在的方法区中找到s1()方法的直接地址,并将这个直接地址记录到StaticCall类的常量池索引为4的常量表中。这个过程叫常量池解析 ,以后再次调用StaticBindDemo.s1时,将直接找到s1方法的字节码;
4、 完成了StaticCall类常量池索引项4的常量表的解析之后,JVM就可以调用s1()方法,并开始解释执行f1()方法中的指令了。
 
通过上面的过程,我们发现经过常量池解析之后,JVM就能够确定要调用的s1()方法具体在内存的什么位置上了。实际上,这个信息在编译阶段就已经在StaticCall类的常量池中记录了下来。这种在编译阶段就能够确定调用哪个方法的方式,我们叫做静态绑定机制
 
注:Java中只有private、static和final修饰的方法以及构造方法是静态绑定。
a、private方法的特点是不能被继承,也就是不存在调用其子类的对象,只能调用对象自身,因此private方法和定义该方法的类绑定在一起。
 
b、static方法又称类方法,类方法属于类文件。它不依赖对象而存在,在调用的时候就已经知道是哪个类的,所以是类方法是属于静态绑定。
 
c、final方法:final方法可以被继承,但是不能被重写,所以也就是说final方法是属于静态绑定的,因为调用的方法是一样的。
 总结:如果一个方法不可被继承或者继承后不可被覆盖,那么这个方法就采用的静态绑定。
 
 
动态绑定
编译器在每次调用方法时都要进行搜索,时间开销相当大。因此虚拟机会预先为每个类创建一个方发表(method table),其中列出了所有方法的签名和实际调用的方法。  
 
public class Father {
public void f1() {
System.out.println("father-f1");
}
 
public void f1(int i) {
System.out.println("father-f1 params-int :" + i);
}
}
 
public class Son extends Father{
public void f1() {
System.out.println("son f1");
}
 
public void f1(char c) {
System.out.println("son-f1 params-c:" + c);
}
}
public class Demo {
public static void main(String[] args) {
Father f = new Son();
f.f1();
//f.f1('c');
}
}

  

 
通过反编译Demo我们来看看jvm是怎么执行的
 

 

其中invokevirtual指令的详细调用过程是这样的:
(1) invokevirtual指令中的#4指的是Demo类的常量池中第4个常量表的索引项。这个常量表(Methodref ) 记录的是方法f1信息的符号引用(包括f1所在的类名,方法名和返回类型)。JVM会首先根据这个符号引用找到调用方法f1的类的全限定名: Father。这是因为调用方法f1的类的对象father声明为Father类型。
(2) 在Father类型的方法表中查找方法f1,如果找到,则将方法f1在方法表中的索引项记录到Demo类的常量池中第4个常量表中(常量池解析 )。这里有一点要注意:如果Father类型方法表中没有方法f1,那么即使Son类型中方法表有,编译的时候也通过不了。因为调用方法f1的类的对象father的声明为Father类型。
(3) 在调用invokevirtual指令前有一个aload_1指令,它会将开始创建在堆中的Son对象的引用压入操作数栈。然后invokevirtual指令会根据这个Son对象的引用首先找到堆中的Son对象,然后进一步找到Son对象所属类型的方法表.
(4) 这是通过第(2)步中解析完成的#4常量表中的方法表的索引项,可以定位到Son类型方法表中的方法f1(),然后通过直接地址找到该方法字节码所在的内存空间。
很明显,根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。这种在程序运行过程中,通过动态创建的对象的方法表来定位方法的方式,我们叫做 动态绑定机制 。
 
动态绑定过程: 
 <1>虚拟机提取对象的实际类型的方法表。 
 <2>虚拟机搜索方法签名,此时虚拟机已经知道应该调用哪种方法。(PS:方法的签名包括了:1.方法名 2.参数的数量和类型~~~~返回类型不是签名的一部分。) 
 <3>虚拟机调用方法
 
PS:由于动态绑定需要在运行时确定执行哪个版本的方法实现或者变量,比起静态绑定起来要耗时。所以在不影响整体设计,我们可以考虑将方法或者变量使用private,static或者final进行修饰。这边优化的内容就涉及到了内联的知识(我们在Java方法内联中专门介绍)。

posted @ 2018-12-20 21:47  J-Star  阅读(11185)  评论(1编辑  收藏  举报