结论
基于基类的调用和基于接口的调用,从性能上来讲,基于基类的调用性能更高 。因为invokevirtual是基于偏移量的方式来查找方法的,而invokeinterface是基于搜索的。
概述
多态是面向对象程序设计的重要特性。多态允许基类的引用指向派生类的对象,而在具体访问时实现方法的动态绑定。
java对方法动态绑定的实现方法主要基于方法表,但是这里分两种调用方式invokevirtual和invokeinterface,即类引用调用和接口引用调用。类引用调用只需要修改方法表的指针就可以实现动态绑定(具有相同签名的方法,在父类、子类的方法表中具有相同的索引号),而接口引用调用需要扫描整个方法表才能实现动态绑定(因为,一个类可以实现多个接口,另外一个类可能只实现一个接口,无法具有相同的索引号。这句如果没有看懂,继续往下看,会有例子。写到这里,感觉自己看书时,有的时候也会不理解,看不懂,思考一段时间,还是不明白,做个标记,继续阅读吧,然后回头再看,可能就豁然开朗。)。
类引用调用的大致过程为:java编译器将java源代码编译成class文件,在编译过程中,会根据静态类型将调用的符号引用写到class文件中。在执行时,JVM根据class文件找到调用方法的符号引用,然后在静态类型的方法表中找到偏移量,然后根据this指针确定对象的实际类型,使用实际类型的方法表,偏移量跟静态类型中方法表的偏移量一样,如果在实际类型的方法表中找到该方法,则直接调用,否则,按照继承关系从下往上搜索。
下面对上面的描述做具体的分析讨论。
JVM的运行时结构
从上图可以看出,当程序运行时,需要某个类时,类载入子系统会将相应的class文件载入到JVM中,并在内部建立该类的类型信息,这个类型信息其实就是class文件在JVM中存储的一种数据结构,他包含着java类定义的所有信息,包括方法代码,类变量、成员变量、以及本博文要重点讨论的方法表
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | <code class = "hljs java" > package org.fan.learn.methodTable; /** * Created by fan on 2016/3/30. */ public class ClassReference { static class Person { @Override public String toString(){ return "I'm a person." ; } public void eat(){ System.out.println( "Person eat" ); } public void speak(){ System.out.println( "Person speak" ); } } static class Boy extends Person{ @Override public String toString(){ return "I'm a boy" ; } @Override public void speak(){ System.out.println( "Boy speak" ); } public void fight(){ System.out.println( "Boy fight" ); } } static class Girl extends Person{ @Override public String toString(){ return "I'm a girl" ; } @Override public void speak(){ System.out.println( "Girl speak" ); } public void sing(){ System.out.println( "Girl sing" ); } } public static void main(String[] args) { Person boy = new Boy(); Person girl = new Girl(); System.out.println(boy); boy.eat(); boy.speak(); //boy.fight(); System.out.println(girl); girl.eat(); girl.speak(); //girl.sing(); } } </code> |
注意,boy.fight();
和 girl.sing();
这两个是有问题的,在IDEA中会提示“Cannot resolve method ‘fight()’”。因为,方法的调用是有静态类型检查的,而boy和girl的静态类型都是Person类型的,在Person中没有fight方法和sing方法。因此,会报错。
执行结果如下:
从上图可以看到,boy.eat()
和 girl.eat()
调用产生的输出都是”Person eat”,因为Boy和Girl中没有override 父类的eat方法。
字节码指令:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <code class = "hljs scss" > public static void main(java.lang.String[]); Code: Stack= 2 , Locals= 3 , Args_size= 1 0 : new # 2 ; //class ClassReference$Boy 3 : dup 4 : invokespecial # 3 ; //Method ClassReference$Boy."<init>":()V 7 : astore_1 8 : new # 4 ; //class ClassReference$Girl 11 : dup 12 : invokespecial # 5 ; //Method ClassReference$Girl."<init>":()V 15 : astore_2 16 : getstatic # 6 ; //Field java/lang/System.out:Ljava/io/PrintStream; 19 : aload_1 20 : invokevirtual # 7 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 23 : aload_1 24 : invokevirtual # 8 ; //Method ClassReference$Person.eat:()V 27 : aload_1 28 : invokevirtual # 9 ; //Method ClassReference$Person.speak:()V 31 : getstatic # 6 ; //Field java/lang/System.out:Ljava/io/PrintStream; 34 : aload_2 35 : invokevirtual # 7 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 38 : aload_2 39 : invokevirtual # 8 ; //Method ClassReference$Person.eat:()V 42 : aload_2 43 : invokevirtual # 9 ; //Method ClassReference$Person.speak:()V 46 : return </init></init></code> |
其中所有的invokevirtual调用的都是Person类中的方法。
下面看看java对象的内存模型:
从上图可以清楚地看到调用方法的指针指向。而且可以看出相同签名的方法在方法表中的偏移量是一样的。这个偏移量只是说Boy方法表中的继承自Object类的方法、继承自Person类的方法的偏移量与Person类中的相同方法的偏移量是一样的,与Girl是没有任何关系的。
下面再看看调用过程,以girl.speak()
方法的调用为例。在我的字节码中,这条指令对应43: invokevirtual #9; //Method ClassReference$Person.speak:()V
,为了便于使用IBM的图,这里采用跟IBM一致的符号引用:invokevirtual #12;
。调用过程图如下所示:
(1)在常量池中找到方法调用的符号引用
(2)查看Person的方法表,得到speak方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
(3)根据this指针确定方法接收者(girl)的实际类型
(4)根据对象的实际类型得到该实际类型对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用;如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Person类)的方法表,同样按照这个偏移量15查看有无该方法。
接口引用调用invokeinterface
代码如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <code class = "hljs axapta" > package org.fan.learn.methodTable; /** * Created by fan on 2016/3/29. */ public class InterfaceReference { interface IDance { void dance(); } static class Person { @Override public String toString() { return "I'm a person" ; } public void speak() { System.out.println( "Person speak" ); } public void eat() { System.out.println( "Person eat" ); } } static class Dancer extends Person implements IDance { @Override public String toString() { return "I'm a Dancer" ; } @Override public void speak() { System.out.println( "Dancer speak" ); } public void dance() { System.out.println( "Dancer dance" ); } } static class Snake implements IDance { @Override public String toString() { return "I'm a Snake" ; } public void dance() { System.out.println( "Snake dance" ); } } public static void main(String[] args) { IDance dancer = new Dancer(); System.out.println(dancer); dancer.dance(); //dancer.speak(); //dancer.eat(); IDance snake = new Snake(); System.out.println(snake); snake.dance(); } } </code> |
上面的代码中dancer.speak(); dancer.eat();
这两句同样不能调用。
执行结果如下所示:
其字节码指令如下所示:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <code class = "hljs scss" > public static void main(java.lang.String[]); Code: Stack= 2 , Locals= 3 , Args_size= 1 0 : new # 2 ; //class InterfaceReference$Dancer 3 : dup 4 : invokespecial # 3 ; //Method InterfaceReference$Dancer."<init>":()V 7 : astore_1 8 : getstatic # 4 ; //Field java/lang/System.out:Ljava/io/PrintStream; 11 : aload_1 12 : invokevirtual # 5 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 15 : aload_1 16 : invokeinterface # 6 , 1 ; //InterfaceMethod InterfaceReference$IDance.dance:()V 21 : new # 7 ; //class InterfaceReference$Snake 24 : dup 25 : invokespecial # 8 ; //Method InterfaceReference$Snake."<init>":()V 28 : astore_2 29 : getstatic # 4 ; //Field java/lang/System.out:Ljava/io/PrintStream; 32 : aload_2 33 : invokevirtual # 5 ; //Method java/io/PrintStream.println:(Ljava/lang/Object;)V 36 : aload_2 37 : invokeinterface # 6 , 1 ; //InterfaceMethod InterfaceReference$IDance.dance:()V 42 : return </init></init></code> |
从上面的字节码指令可以看到,dancer.dance();
和snake.dance();
的字节码指令都是invokeinterface #6, 1; //InterfaceMethod InterfaceReference$IDance.dance:()V
。
为什么invokeinterface指令会有两个参数呢?
对象的内存模型如下所示:
从上图可以看到IDance接口中的方法dance()在Dancer类的方法表中的偏移量跟在Snake类的方法表中的偏移量是不一样的,因此无法仅根据偏移量来进行方法的调用。(这句话在理解时,要注意,只是为了强调invokeinterface在查找方法时不再是基于偏移量来实现的,而是基于搜索的方式。)应该这么说,dance方法在IDance方法表(如果有的话)中的偏移量与在Dancer方法表中的偏移量是不一样的。
因此,要在Dancer的方法表中找到dance方法,必须搜索Dancer的整个方法表。
下面写一个,如果Dancer中没有重写(override)toString方法,会发生什么?
代码如下:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <code class = "hljs axapta" > package org.fan.learn.methodTable; /** * Created by fan on 2016/3/29. */ public class InterfaceReference { interface IDance { void dance(); } static class Person { @Override public String toString() { return "I'm a person" ; } public void speak() { System.out.println( "Person speak" ); } public void eat() { System.out.println( "Person eat" ); } } static class Dancer extends Person implements IDance { // @Override // public String toString() { // return "I'm a Dancer"; // } @Override public void speak() { System.out.println( "Dancer speak" ); } public void dance() { System.out.println( "Dancer dance" ); } } static class Snake implements IDance { @Override public String toString() { return "I'm a Snake" ; } public void dance() { System.out.println( "Snake dance" ); } } public static void main(String[] args) { IDance dancer = new Dancer(); System.out.println(dancer); dancer.dance(); //dancer.speak(); //dancer.eat(); IDance snake = new Snake(); System.out.println(snake); snake.dance(); } } </code> |
执行结果如下:
可以看到System.out.println(dancer);
调用的是Person的toString方法。
内存模型如下所示:
结束语
这篇博文讨论了invokevirtual和invokeinterface的内部实现的区别,以及override的实现原理。下一步,打算讨论下invokevirtual的具体实现细节,如:如何实现符号引用到直接引用的转换的?可能会看下OpenJDK底层的C++实现。