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 静态分派
对于这个静态分派,撒迦很喜欢叫做非虚方法分派(当然按照他自己的说法就是:我不喜欢叫静态分派).
首先,解释一下什么叫虚方法.虚方法的概念有点难说,不过把什么是非虚方法说明一下,其他的就是虚方法啦.哈哈
由于非虚方法不能被override,所以自然也不会产生子类复写的多态效果.这样的话,方法被调用的入口只可能是一个.而且编译器可知.也就是说,jvm需要执行哪个方法是在编译器就已经确定.且在运行期不会变化.很具体的例子就是方法的重载.
看如下例子,摘自 <深入JAVA虚拟机-jvm高级特性与最佳实践>
- public class StaticDispatch {
- static abstract class Human{
- }
- static class Man extends Human{
- }
- static class Woman extends Human{
- }
- public void sayHello(Human human){
- System.out.println("human say hello");
- }
- public void sayHello(Man man){
- System.out.println("man say hello");
- }
- public void sayHello(Woman woman){
- System.out.println("woman say hello");
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch sd = new StaticDispatch();
- sd.sayHello(man);
- sd.sayHello(woman);
- }
- }
最后的输出是
human say hello
这个就是很典型的静态分派.看这段代码
Human woman = new Woman();
其中的Human 称为变量的静态类型,而后面的Man称为变量的实际类型. 静态类型是在编译器可见的,而动态类型必须在运行期才知道.再分析这段调用的方法
sd.sayHello(man);
sd.sayHello(woman);
我们看到,调用方法的接受者是确定的,都是sd.在静态分派中,jvm如何确定具体调用哪个目标方法就完全取决于传入参数的数量和数据类型.而且是根据数据的静态类型..正因为如此,这两个sayHello方法,最后都调用了public void sayHello(Human human);方法.
但是,仔细看会发现,我举的这个例子,虽然确实是通过静态分派的,但是具体的方法却是虚方法..也就是说,
其实非虚方法的静态分派是完全合理的,后面会再举一个例子,来确定只要是非虚方法,肯定是通过静态分派的.
本节最后的问题是
这个问题曾经让我有过困惑.因为上面这个重载的例子中,
- sd.sayHello(man);
- sd.sayHello(woman);
这两个sayHello方法都是用invokevirtual 指令(关于这个指令,后面会开专门的一节说明)的,那么其实完全可以采用动态分派,根据man 和 woman 的实际类型来决定调用哪个方法.但是实际上jvm缺没这么做.一直等我在仔细看了Java语言"单分派还是多分派"这个内容以后,才有了答案.下面会专门开一节说这个单分派和多分派.这个问题也在后面解答.
2 动态分派
可以说,动态方法分派是Java实现多态的一个重要基础.因为,它是Java多态之一----重写的基础.看下面的代码,,摘自 <深入JAVA虚拟机-jvm高级特性与最佳实践>
- public class DynamicDispatch {
- static abstract class Human{
- protected abstract void sayHello();
- }
- static class Man extends Human{
- @Override
- protected void sayHello() {
- System.out.println("man say hello");
- }
- }
- static class Woman extends Human{
- @Override
- protected void sayHello() {
- System.out.println("woman say hello");
- }
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- Human man = new Man();
- Human woman = new Woman();
- man.sayHello();
- woman.sayHello();
- }
- }
woman say hello
只要有一点Java基础的人基本都能看懂这段代码.一个非常简单的重写.具体看它的结果,很明显这里已经不是静态分派了.因为man和woman在编译器都是Human类型,如果是静态分派,那么这两个调用的方法应该是同一个.但是实际上,它们却调用了对应的真实类型的方法.这就是动态分派.
3 invokespecial和invokevirtual指令
说这个,最主要是由于上面说的那个讨论引起的(详情http://hllvm.group.iteye.com/group/topic/27064).代码还是放上来吧.
代码1
- public class SuperTest {
- public static void main(String[] args) {
- new Sub().exampleMethod();
- }
- }
- class Super {
- <span style="color: #ff0000;">private</span> void interestingMethod() {
- System.out.println("Super's interestingMethod");
- }
- void exampleMethod() {
- interestingMethod();
- }
- }
- class Sub extends Super {
- void interestingMethod() {
- System.out.println("Sub's interestingMethod");
- }
- }
代码2
- public class SuperTest {
- public static void main(String[] args) {
- new Sub().exampleMethod();
- }
- }
- class Super {
- void interestingMethod() {
- System.out.println("Super's interestingMethod");
- }
- void exampleMethod() {
- interestingMethod();
- }
- }
- class Sub extends Super {
- void interestingMethod() {
- System.out.println("Sub's interestingMethod");
- }
- }
代码一与代码二,只有一个区别,就是在代码一中Super类的interestingMethod方法的修饰符多一个private.根据执行最后的结果来看却是直接造成了方法分派的不同.一个执行了父类的interestingMethod方法,而一个执行了子类的interestingMethod方法.
对于这个例子,撒迦的回答比较明确
Java里只有非private的成员方法是虚方法。
所以你会留意到在顶楼例子的第一个版本里,exampleMethod()是用invokespecial来调用interestingMethod()的;而第二个版本里则是用invokevirtual。
在本文的开头已经解释了"什么是虚方法"这个问题.可以知道,代码一中Super类的interestingMethod方法是非虚方法(因为第一个是private方法),而代码二则是虚方法.可以明确的是
所以,在代码一中,使用静态分派,Super类中的exampleMethod方法调用的是自己类中的interestingMethod方法.这个是编译器就已经确定的.而代码二中,exampleMethod方法执行哪个interestingMethod方法就需要看真实对象是哪个.在本例中,真实对象肯定是Sub类.所以就调用Sub类的interestingMethod方法.
上面的这一段分析很简单,我们可以通过javap输出看看对应的信息(只需要看Super类的输出就可以了.代码一和代码二的唯一区别就是Super类的interestingMethod方法修饰符)
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
}
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 .
· invokevirtual - 用于调用一般实例方法(包括声明为final但不为private的实例方法)
其中
到这里,我们可以明确的是,使用invokespecial 指令的肯定是静态方法分配的,但是使用invokevirtual却还不一定()..我们可以看一下本文说静态分配的那个例子的javap输出(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 与方法分派没有直接的关系,但是这两个指令与虚方法之间还是有非常大的联系的.
上面说的两个例外说明一下, static修饰的方法通过invokestatic 指令来调用.
而final修饰且非private的方法也是用invokevirtual指令来调用的.这个可以看下撒迦的说明
关键词:分离编译,二进制兼容性
A.java
- public class A {
- public void foo() { /* ... */ }
- }
B.java
- public class B extends A {
- public void foo() { /* ... */ }
- }
C.java
- public class C extends B {
- public final void foo() { /* ... */ }
- }
这样的话有3个源码文件,它们可以分别编译。三个类有继承关系,每个都有自己的foo()的实现。其中C.foo()是final的。
那么如果在别的什么地方,
- A a = getA();
- a.foo();
这个a.foo()应该使用invokevirtual是很直观的对吧?
而这个实际的调用目标也有可能是C.foo(),对吧?
所以为了设计的简单性,以及更好的二进制兼容性……(此处省略
4 单分派与多分派
首先解释一下这两个概念.在《Java与模式》中的译文中提出了宗量这个概念。
“方法的接受者”这个本文上面已经有说明了,而“方法的参数”就是指方法的参数类型和个数。
其实这个定义并不好理解。我找不到其他好的例子来说明这个,所以采用《深入Java虚拟机--JVM高级特性与最佳实践》的例子说明,包括后面的说明很多都来自此书
- public class Dispatcher {
- static class QQ {
- }
- static class _360 {
- }
- public static class Father {
- public void hardChoice(QQ qq) {
- System.out.println("father choose qq");
- }
- public void hardChoice(_360 _360) {
- System.out.println("father choose 360");
- }
- }
- public static class Son extends Father{
- public void hardChoice(QQ qq) {
- System.out.println("son choose qq");
- }
- public void hardChoice(_360 _360) {
- System.out.println("son choose 360");
- }
- }
- public static void main(String[] args) {
- Father father = new Father();
- Father son = new Son();
- father.hardChoice(new _360());
- son.hardChoice(new QQ());
- }
- }
son choose qq
上面的例子中 ,我们需要关心的主要是这两行代码
- father.hardChoice(new _360());
- son.hardChoice(new QQ());
我们分别从编译阶段和运行阶段分别分析这个分派的过程。在编译阶段,jvm在选择哪个hardChoice方法的时候有两点依据:一是静态类型是Fatcher还是Son.二是方法参数的QQ还是360。根据这两点,在静态编译的时候,这两行代码会被翻译成 Father.hardChoice(360)和 Father.hardChoice(QQ).到这里,我们就可以知道,
在运行阶段,执行 son.hardChoice(new QQ()); 的时候,由于编译器已经在编译阶段决定目标方法的签名必须是 “hardChoice(QQ)”,jvm此时不会关心传递过来的QQ参数到底是 “腾讯QQ”还是“奇瑞QQ”,因为这个时候参数的静态类型,实际类型都不会对方法的分派构成任何影响,唯一可以影响jvm进行方法分派的只有该方法的接受者,也就是son。这个时候,其实就是一个宗量作为分派的选择,也就是
我想应该很多人对静态多分派的说明不会有疑义,而对动态单分派会有一些疑问。因为我第一次看的时候也觉得,就上面这个QQ和360的例子并不能十分好的解释在运行期动态分派的时候,jvm只对方法的接受者敏感,而对方法的参数无视。我想大家是否有想到我在本文第一节说静态分派的时候提到的那个问题:
在静态分派的那个重载的例子中:
- Human man = new Man();
- Human woman = new Woman();
- StaticDispatch sd = new StaticDispatch();
- sd.sayHello(man);
- sd.sayHello(woman);
human say hello
可以想想,为什么最后都会执行 Human类的sayHello方法。这里就可以有很明确的解释了,就是因为Java语言是动态单分派的!在编译阶段 man和woman都是Human类型,所以在运行时调用sd.sayHello(man);和 sd.sayHello(woman);的时候,jvm已经不关心sayHello方法参数的真实类型是什么了,它只关心具体的接受者是什么。那么,结果显而易见,他们都会调用Human类的sayHello方法。所以,
最后,摘录下《深入Java虚拟机--JVM高级特性与最佳实践》中关于动态分派的说明:
最后,再次感谢撒迦与《深入Java虚拟机--JVM高级特性与最佳实践》对本文的大力支持。哈哈
- JVM之方法调用-分派
- JVM 方法调用之动态分派
- JVM 方法调用之动态分派
- JVM方法分派
- JVM方法分派:静态多分派、动态单分派
- 深入理解jvm之分派
- java方法调用之单分派与多分派(二)
- 深入理解JVM之七:静态分派与动态分派
- 深入理解JVM之七:静态分派与动态分派
- 深入理解JVM之七:静态分派与动态分派
- java方法调用中的单分派与多分派
- java中的方法调用-解析与分派
- 方法分派
- 方法分派
- JVM学习笔记之方法调用
- JVM解析与分派
- 关于JVM中的分派
- 【JVM】静态分派和动态分派
- python 读写 ndarray
- python中的shape计算
- Unity3d和Android的互相调用(二)
- linux内核裁剪选项说明
- json
- JVM之方法调用-分派
- Phinecos(洞庭散人) 专注于开源技术的研究与应用 Nehe的OpenGL框架(MFC版)
- 设计模式系列之扉页
- nachos-Runnable和Thread的区别
- Android实现多点触控,自由缩放图片
- POJ2406 Power Strings
- 线性回归(2)缩减系数理解
- 1076. Forwards on Weibo (30)
- numpy