JVM之方法调用-分派

来源:互联网 发布:算法设计与分析分治法 编辑:程序博客网 时间:2024/06/18 10:52

说明:这两天遇到的一些Java方法分派的问题,结合自己书上看的,google的,还有撒迦教我的,做一个总结吧.望指正.

 

写道
方法分派指的是虚拟机如何确定应该执行哪个方法!

 

很多的内容可以参加撒迦的这篇博文 : http://rednaxelafx.iteye.com/blog/652719

我这篇里很多概念的解释都摘自上面的博文,所以,我就不一一指出啦.在此感谢撒迦的帮助.

还有一些讲解(包括代码)来自 <深入JAVA虚拟机-jvm高级特性与最佳实践>,很好一本书,推荐对jvm有兴趣的同学购买

另外 http://hllvm.group.iteye.com/group/topic/27064 这个帖子也可能帮助大家对方法分派有所了解.

1 静态分派

    对于这个静态分派,撒迦很喜欢叫做非虚方法分派(当然按照他自己的说法就是:我不喜欢叫静态分派).

    首先,解释一下什么叫虚方法.虚方法的概念有点难说,不过把什么是非虚方法说明一下,其他的就是虚方法啦.哈哈

 
非虚方法是所有的类方法(也就是申明为static的方法) + 所有声明为final或private的实例方法.

 

    由于非虚方法不能被override,所以自然也不会产生子类复写的多态效果.这样的话,方法被调用的入口只可能是一个.而且编译器可知.也就是说,jvm需要执行哪个方法是在编译器就已经确定.且在运行期不会变化.很具体的例子就是方法的重载.

    看如下例子,摘自 <深入JAVA虚拟机-jvm高级特性与最佳实践>

 

Java代码  收藏代码
  1. public class StaticDispatch {  
  2.       
  3.     static abstract class Human{  
  4.           
  5.     }  
  6.       
  7.     static class Man extends Human{  
  8.           
  9.     }  
  10.       
  11.     static class Woman extends Human{  
  12.           
  13.     }  
  14.   
  15.     public void sayHello(Human human){  
  16.         System.out.println("human say hello");  
  17.     }  
  18.       
  19.     public void sayHello(Man man){  
  20.         System.out.println("man say hello");  
  21.     }  
  22.       
  23.     public void sayHello(Woman woman){  
  24.         System.out.println("woman say hello");  
  25.     }  
  26.       
  27.     /** 
  28.      * @param args 
  29.      */  
  30.     public static void main(String[] args) {  
  31.         Human man = new Man();  
  32.         Human woman = new Woman();  
  33.         StaticDispatch sd = new StaticDispatch();  
  34.         sd.sayHello(man);  
  35.         sd.sayHello(woman);  
  36.     }  
  37.   
  38. }  

 

最后的输出是

console 写道
human say hello 
human say hello

 

这个就是很典型的静态分派.看这段代码

 

Human man = new Man(); 
Human woman = new Woman();

    

     其中的Human 称为变量的静态类型,而后面的Man称为变量的实际类型. 静态类型是在编译器可见的,而动态类型必须在运行期才知道.再分析这段调用的方法

  

StaticDispatch sd = new StaticDispatch(); 
sd.sayHello(man); 
sd.sayHello(woman);

 

    我们看到,调用方法的接受者是确定的,都是sd.在静态分派中,jvm如何确定具体调用哪个目标方法就完全取决于传入参数的数量和数据类型.而且是根据数据的静态类型..正因为如此,这两个sayHello方法,最后都调用了public void sayHello(Human human);方法.

 

   但是,仔细看会发现,我举的这个例子,虽然确实是通过静态分派的,但是具体的方法却是虚方法..也就是说,

 
虚方法也可能是被静态分派的.特别注意,重载就是通过静态分派的

 

   其实非虚方法的静态分派是完全合理的,后面会再举一个例子,来确定只要是非虚方法,肯定是通过静态分派的.

   本节最后的问题是

写道
Java语言中方法重载采用静态分派是JVM规范规定的还是语言级别的规定?

 

    这个问题曾经让我有过困惑.因为上面这个重载的例子中,

Java代码  收藏代码
  1. sd.sayHello(man);   
  2. sd.sayHello(woman);  

    这两个sayHello方法都是用invokevirtual 指令(关于这个指令,后面会开专门的一节说明)的,那么其实完全可以采用动态分派,根据man 和 woman 的实际类型来决定调用哪个方法.但是实际上jvm缺没这么做.一直等我在仔细看了Java语言"单分派还是多分派"这个内容以后,才有了答案.下面会专门开一节说这个单分派和多分派.这个问题也在后面解答. 

 

2 动态分派 

    可以说,动态方法分派是Java实现多态的一个重要基础.因为,它是Java多态之一----重写的基础.看下面的代码,,摘自 <深入JAVA虚拟机-jvm高级特性与最佳实践>

Java代码  收藏代码
  1. public class DynamicDispatch {  
  2.       
  3.     static abstract class Human{  
  4.         protected abstract void sayHello();  
  5.     }  
  6.       
  7.     static class Man extends Human{  
  8.   
  9.         @Override  
  10.         protected void sayHello() {  
  11.             System.out.println("man say hello");  
  12.         }  
  13.           
  14.     }  
  15.       
  16.     static class Woman extends Human{  
  17.   
  18.         @Override  
  19.         protected void sayHello() {  
  20.             System.out.println("woman say hello");  
  21.         }  
  22.           
  23.     }  
  24.   
  25.     /** 
  26.      * @param args 
  27.      */  
  28.     public static void main(String[] args) {  
  29.         Human man = new Man();  
  30.         Human woman = new Woman();  
  31.         man.sayHello();  
  32.         woman.sayHello();  
  33.     }  
  34.   
  35. }  

 

console 写道
man say hello 
woman say hello 

 

    只要有一点Java基础的人基本都能看懂这段代码.一个非常简单的重写.具体看它的结果,很明显这里已经不是静态分派了.因为man和woman在编译器都是Human类型,如果是静态分派,那么这两个调用的方法应该是同一个.但是实际上,它们却调用了对应的真实类型的方法.这就是动态分派.

 

3 invokespecial和invokevirtual指令

    说这个,最主要是由于上面说的那个讨论引起的(详情http://hllvm.group.iteye.com/group/topic/27064).代码还是放上来吧.

 

   代码1

Java代码  收藏代码
  1. public class SuperTest {  
  2.     public static void main(String[] args) {  
  3.         new Sub().exampleMethod();  
  4.     }  
  5. }  
  6.   
  7. class Super {  
  8.     <span style="color: #ff0000;">private</span> void interestingMethod() {  
  9.         System.out.println("Super's interestingMethod");  
  10.     }  
  11.   
  12.     void exampleMethod() {  
  13.         interestingMethod();  
  14.     }  
  15. }  
  16.   
  17. class Sub extends Super {  
  18.   
  19.     void interestingMethod() {  
  20.         System.out.println("Sub's interestingMethod");  
  21.     }  
  22. }  

 

console输出
Super's interestingMethod 

 

  代码2

Java代码  收藏代码
  1. public class SuperTest {  
  2.     public static void main(String[] args) {  
  3.         new Sub().exampleMethod();  
  4.     }  
  5. }  
  6.   
  7. class Super {  
  8.     void interestingMethod() {  
  9.         System.out.println("Super's interestingMethod");  
  10.     }  
  11.   
  12.     void exampleMethod() {  
  13.         interestingMethod();  
  14.     }  
  15. }  
  16.   
  17. class Sub extends Super {  
  18.   
  19.     void interestingMethod() {  
  20.         System.out.println("Sub's interestingMethod");  
  21.     }  
  22. }  

 

console输出 写道
Sub's interestingMethod

 

    代码一与代码二,只有一个区别,就是在代码一中Super类的interestingMethod方法的修饰符多一个private.根据执行最后的结果来看却是直接造成了方法分派的不同.一个执行了父类的interestingMethod方法,而一个执行了子类的interestingMethod方法.

    对于这个例子,撒迦的回答比较明确

写道
关键点在于“Java里什么是虚方法”以及“虚方法如何分派”。 
Java里只有非private的成员方法是虚方法。 

所以你会留意到在顶楼例子的第一个版本里,exampleMethod()是用invokespecial来调用interestingMethod()的;而第二个版本里则是用invokevirtual。

    在本文的开头已经解释了"什么是虚方法"这个问题.可以知道,代码一中Super类的interestingMethod方法是非虚方法(因为第一个是private方法),而代码二则是虚方法.可以明确的是

 

写道
非虚方法肯定是用静态分派

 

    所以,在代码一中,使用静态分派,Super类中的exampleMethod方法调用的是自己类中的interestingMethod方法.这个是编译器就已经确定的.而代码二中,exampleMethod方法执行哪个interestingMethod方法就需要看真实对象是哪个.在本例中,真实对象肯定是Sub类.所以就调用Sub类的interestingMethod方法.

   

    上面的这一段分析很简单,我们可以通过javap输出看看对应的信息(只需要看Super类的输出就可以了.代码一和代码二的唯一区别就是Super类的interestingMethod方法修饰符)

      

 

代码一的Super类javap输出
Compiled from "SuperTest.java" 
class Super { 
Super(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

void exampleMethod(); 
Code: 
0: aload_0 
1: ldc #5 // String aa 
3: invokespecial #6 // Method interestingMethod:(Ljava/l 
ang/String;)I 
6: pop 
7: return 
}

 

代码二的Super类javap输出 写道
Compiled from "SuperTest.java" 
class Super { 
Super(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

int interestingMethod(java.lang.String); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #3 // String Super's interestingMethod 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: iconst_1 
9: ireturn 

void exampleMethod(); 
Code: 
0: aload_0 
1: ldc #5 // String aa 
3: invokevirtual #6 // Method interestingMethod:(Ljava/l 
ang/String;)I 
6: pop 
7: return 
}

 

    两边的不同我通过加粗来说明了.正如撒迦说的,在Super类的exampleMethod方法中调用interestingMethod方法的指令是不同的,代码一采用的是invokespecial 而代码二采用的是invokevirtual .

 

写道
· invokespecial - super方法调用、private方法调用与构造器调用 
· invokevirtual - 用于调用一般实例方法(包括声明为final但不为private的实例方法) 

    

其中

   

写道
invokespecial调用的目标必然是可以静态绑定的,因为它们都无法参与子类型多态;invokevirtual的则一般需要做运行时绑定

 

    到这里,我们可以明确的是,使用invokespecial 指令的肯定是静态方法分配的,但是使用invokevirtual却还不一定()..我们可以看一下本文说静态分配的那个例子的javap输出(StaticDispatch类)

 

StaticDispatch类的javap输出
public class StaticDispatch { 
public StaticDispatch(); 
Code: 
0: aload_0 
1: invokespecial #1 // Method java/lang/Object."<init>": 
()V 
4: return 

public void sayHello(StaticDispatch$Human); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #3 // String human say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public void sayHello(StaticDispatch$Man); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #5 // String man say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public void sayHello(StaticDispatch$Woman); 
Code: 
0: getstatic #2 // Field java/lang/System.out:Ljava/ 
io/PrintStream; 
3: ldc #6 // String woman say hello 
5: invokevirtual #4 // Method java/io/PrintStream.printl 
n:(Ljava/lang/String;)V 
8: return 

public static void main(java.lang.String[]); 
Code: 
0: new #7 // class StaticDispatch$Man 
3: dup 
4: invokespecial #8 // Method StaticDispatch$Man."<init> 
":()V 
7: astore_1 
8: new #9 // class StaticDispatch$Woman 
11: dup 
12: invokespecial #10 // Method StaticDispatch$Woman."<ini 
t>":()V 
15: astore_2 
16: new #11 // class StaticDispatch 
19: dup 
20: invokespecial #12 // Method "<init>":()V 
23: astore_3 
24: aload_3 
25: aload_1 
26: invokevirtual #13 // Method sayHello:(LStaticDispatch$ 
Human;)V 
29: aload_3 
30: aload_2 
31: invokevirtual #13 // Method sayHello:(LStaticDispatch$ 
Human;)V 
34: return 
}

 

    由此,我们可以看到,其实invokevirtual  也有可能是静态分派的.也就是说

写道
invokevirtual 指令与动态分派没有直接的联系.但是invokespecial调用的目标必然是可以静态绑定的

  

    本节的最后,必须还要说一下invokevirtual ,invokespecial指令与虚方法之间的关系.虽然invokevirtual 与方法分派没有直接的关系,但是这两个指令与虚方法之间还是有非常大的联系的.

写道
所有invokespecial指令调用的方法都是非虚方法,而非虚方法也都是用invokespecial方法调用的.但是,后半句有两个例外,static修饰的方法与final修饰且非private的方法
虚方法都是通过invokevirtual指令来调用的

    

     上面说的两个例外说明一下, static修饰的方法通过invokestatic 指令来调用.

     而final修饰且非private的方法也是用invokevirtual指令来调用的.这个可以看下撒迦的说明

RednaxelaFX 写道
直接把答案说出来就不有趣了。让我举个例子来诱导一下。 
关键词:分离编译,二进制兼容性 

A.java 
Java代码  收藏代码
  1. public class A {  
  2.   public void foo() { /* ... */ }  
  3. }  


B.java 
Java代码  收藏代码
  1. public class B extends A {  
  2.   public void foo() { /* ... */ }  
  3. }  


C.java 
Java代码  收藏代码
  1. public class C extends B {  
  2.   public final void foo() { /* ... */ }  
  3. }  


这样的话有3个源码文件,它们可以分别编译。三个类有继承关系,每个都有自己的foo()的实现。其中C.foo()是final的。 

那么如果在别的什么地方, 
Java代码  收藏代码
  1. A a = getA();  
  2. a.foo();  

这个a.foo()应该使用invokevirtual是很直观的对吧? 
而这个实际的调用目标也有可能是C.foo(),对吧? 

所以为了设计的简单性,以及更好的二进制兼容性……(此处省略

 4 单分派与多分派

    首先解释一下这两个概念.在《Java与模式》中的译文中提出了宗量这个概念。

 

写道
方法的接受者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派。

     “方法的接受者”这个本文上面已经有说明了,而“方法的参数”就是指方法的参数类型和个数。 

    其实这个定义并不好理解。我找不到其他好的例子来说明这个,所以采用《深入Java虚拟机--JVM高级特性与最佳实践》的例子说明,包括后面的说明很多都来自此书

 

Java代码  收藏代码
  1. public class Dispatcher {  
  2.   
  3.     static class QQ {  
  4.     }  
  5.   
  6.     static class _360 {  
  7.     }  
  8.   
  9.     public static class Father {  
  10.         public void hardChoice(QQ qq) {  
  11.             System.out.println("father choose qq");  
  12.         }  
  13.   
  14.         public void hardChoice(_360 _360) {  
  15.             System.out.println("father choose 360");  
  16.         }  
  17.     }  
  18.       
  19.     public static class Son extends Father{  
  20.         public void hardChoice(QQ qq) {  
  21.             System.out.println("son choose qq");  
  22.         }  
  23.   
  24.         public void hardChoice(_360 _360) {  
  25.             System.out.println("son choose 360");  
  26.         }  
  27.     }  
  28.   
  29.     public static void main(String[] args) {  
  30.         Father father = new Father();  
  31.         Father son = new Son();  
  32.         father.hardChoice(new _360());  
  33.         son.hardChoice(new QQ());  
  34.           
  35.     }  
  36.   
  37. }  

 

     

console输出 写道
father choose 360 
son choose qq 

 

    上面的例子中 ,我们需要关心的主要是这两行代码

Java代码  收藏代码
  1. father.hardChoice(new _360());  
  2. son.hardChoice(new QQ());  

     我们分别从编译阶段和运行阶段分别分析这个分派的过程。在编译阶段,jvm在选择哪个hardChoice方法的时候有两点依据:一是静态类型是Fatcher还是Son.二是方法参数的QQ还是360。根据这两点,在静态编译的时候,这两行代码会被翻译成 Father.hardChoice(360)和 Father.hardChoice(QQ).到这里,我们就可以知道,

 

写道
Java是静态多分派的语言

 

    在运行阶段,执行 son.hardChoice(new QQ()); 的时候,由于编译器已经在编译阶段决定目标方法的签名必须是 “hardChoice(QQ)”,jvm此时不会关心传递过来的QQ参数到底是 “腾讯QQ”还是“奇瑞QQ”,因为这个时候参数的静态类型,实际类型都不会对方法的分派构成任何影响,唯一可以影响jvm进行方法分派的只有该方法的接受者,也就是son。这个时候,其实就是一个宗量作为分派的选择,也就是

 

写道
Java是动态单分派的语言

 

   我想应该很多人对静态多分派的说明不会有疑义,而对动态单分派会有一些疑问。因为我第一次看的时候也觉得,就上面这个QQ和360的例子并不能十分好的解释在运行期动态分派的时候,jvm只对方法的接受者敏感,而对方法的参数无视。我想大家是否有想到我在本文第一节说静态分派的时候提到的那个问题:

写道
Java语言中方法重载采用静态分派是JVM规范规定的还是语言级别的规定? 

    在静态分派的那个重载的例子中:

Java代码  收藏代码
  1. Human man = new Man();  
  2. Human woman = new Woman();  
  3. StaticDispatch sd = new StaticDispatch();  
  4. sd.sayHello(man);  
  5. sd.sayHello(woman);   
console输出 写道
human say hello 
human say hello

    可以想想,为什么最后都会执行 Human类的sayHello方法。这里就可以有很明确的解释了,就是因为Java语言是动态单分派的!在编译阶段 man和woman都是Human类型,所以在运行时调用sd.sayHello(man);和 sd.sayHello(woman);的时候,jvm已经不关心sayHello方法参数的真实类型是什么了,它只关心具体的接受者是什么。那么,结果显而易见,他们都会调用Human类的sayHello方法。所以,

   

写道
Java语言对重载采用静态分派的原因在于Java是动态单分派的!

 

    最后,摘录下《深入Java虚拟机--JVM高级特性与最佳实践》中关于动态分派的说明:

写道
今天(JDK1.6时期)的Java语言是一门静态多分派,动态多分派的语言。强调“今天的Java语言”是因为这个结论未必会恒久不变,C#在3.0以及之前的版本与Java医院也是动态单分派的语言,但是在C#4.0中引入dynamic类型以后,就可以方便地实现动态多分派。Java也已经在JSR-292中开始规划对动态语言的支持了,日后很可能提供类似的动态类型功能。

 

  

最后,再次感谢撒迦与《深入Java虚拟机--JVM高级特性与最佳实践》对本文的大力支持。哈哈 

1 0
原创粉丝点击