`
chenjingbo
  • 浏览: 460032 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Java方法分派

阅读更多

说明:这两天遇到的一些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高级特性与最佳实践>

 

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);
	}

}

 

最后的输出是

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规范规定的还是语言级别的规定?

 

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

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();
	}

}

 

console 写道
man say hello
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 {
	private void interestingMethod() {
		System.out.println("Super's interestingMethod");
	}

	void exampleMethod() {
		interestingMethod();
	}
}

class Sub extends Super {

	void interestingMethod() {
		System.out.println("Sub's interestingMethod");
	}
}

 

console输出
Super'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");
	}
}

 

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
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());
		
	}

}

 

     

console输出 写道
father choose 360
son choose qq

 

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

father.hardChoice(new _360());
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规范规定的还是语言级别的规定?

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

Human man = new Man();
Human woman = new Woman();
StaticDispatch sd = new StaticDispatch();
sd.sayHello(man);
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高级特性与最佳实践》对本文的大力支持。哈哈   

分享到:
评论
6 楼 liuzeji 2014-08-16  
非虚方法分派?深入java虚拟机那本书上说,非虚方法在解析的时候就可以确定了,只用虚方法才需要分派调用
5 楼 _____LG 2014-01-07  
不错,谢谢分享!
4 楼 scriptguy 2012-08-27  
是不是可以这样理解:
1、在编译的时候,确定调用哪个方法(重载)
2、运行时确定调用谁的方法(多态)

结合本文,
1.Human man = new Man();   
2.Human woman = new Woman();   
3.StaticDispatch sd = new StaticDispatch();   
4.sd.sayHello(man);   
5.sd.sayHello(woman); 

这段代码,我是这样理解的:
在编译的时候进行了静态分派,确定调用
public void sayHello(Human human){   
    System.out.println("human say hello");   
}
这个方法
在运行时进行动态分派,确定调用的是StaticDispatch实例的方法

是不是就可以说,虚函数的调用都是先在编译期进行静态分派,然后在运行时进行动态分派的呢?
3 楼 chenjingbo 2011-11-15  
wupuyuan 写道
忘了说了,一般不公开非免费的书中的内容的……当然老周应该不介意就是了,呵呵


哈哈,我给他的书提了这么多BUG,我就稍微抄下他的几行代码,不怕不怕.嘿嘿
2 楼 wupuyuan 2011-11-15  
忘了说了,一般不公开非免费的书中的内容的……当然老周应该不介意就是了,呵呵
1 楼 wupuyuan 2011-11-15  
恩,写的不错!

相关推荐

    java 任务分派信息管理系统 数据结构

    java 任务分派信息管理系统 数据结构 [问题描述] 参考办公室任务分派工作的相关信息需求,提供任务,员工,任务执行等信息的管理功 能。 [实现要求] 能够根据员工的技能和空闲情况来分派任务。并能够对每月的员工的...

    Java的动态分派和静态分派的实现

    Java 的动态分派和静态分派也是 Java 方法的执行原理。在 Java 中,方法的调用是使用符号引用来表示的。当字节码被 JVM 加载之后,符号引用才会被替换为对应方法在方法区的真实内存地址。在替换之前,由于 Java 的...

    Java模拟双分派DoubleDispatch

    分派/dispatch是指如何给一个消息绑定其方法体。Java、C#等仅仅支持单分派(singledispatch)而不支持双分派(double dispatch)。【相关概念,参考《设计模式.5.11访问者模式》p223】对于消息a.foo(b),假设有父类X...

    请求重定向个请求分派

    请求重定向和请求分派技术详解 请求重定向和请求分派是 MVC 架构中关键的技术,它们在 Web 应用程序中扮演着重要的角色。...了解它们的概念、实现方法和应用场景是每个 Java Web 开发者所必需的。

    【深入Java虚拟机(5)】多态性实现机制-静态分派与动

    每个Java对象都包含一个指向其类方法表的指针,当调用虚方法时,JVM会通过这个指针找到相应的方法实现并执行。非虚方法(如final或static方法)则不经过动态分派。 了解静态分派和动态分派对于理解Java的多态行为至...

    java客户端调用webservice所调用的axis1.4包和方法调用

    本方法是用axis1.4技术,实现java客户端调用webservice。已经可实现过可行的,如果不行可加我QQ号302633进行详细解析。

    java 任务分配样例3

    在Java开发中,Quartz 2是一个非常强大的作业调度库,它允许开发者安排任务并在特定时间执行。在本文中,我们将深入探讨如何使用`@PersistJobDataAfterExecution`和`@DisallowConcurrentExecution`注解来通过...

    Java的动态绑定与双分派_动力节点Java学院整理

    Java的动态绑定与双分派是面向对象编程中两个重要的概念,它们关乎程序在运行时如何选择正确的方法执行。动态绑定,又称晚期绑定或虚函数调用,是Java等面向对象语言的一种特性,它允许在运行时根据对象的实际类型...

    java--基于ssm网约垃圾分类员分派管理系统.zip

    在这个"java--基于ssm网约垃圾分类员分派管理系统"项目中,我们可以深入探讨以下几个核心知识点: 1. **Spring框架**:Spring是Java企业级应用的核心框架,它提供了一个全面的编程和配置模型,使得开发人员能够更...

    [inside hotspot] java方法调用的StubCode1

    虚调用是最常见的动态分派调用方式,其中具体调用哪个方法取决于运行时对象的实际类型。 - **定义**: ```cpp static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name, Symbol* ...

    69丨访问者模式(下):为什么支持双分派的语言不需要访问者模式?1

    双分派是指在运行时,不仅根据对象的类型决定调用哪个方法,还根据方法参数的类型决定调用哪个具体实现。这与单分派(Single Dispatch)形成对比,单分派仅根据对象的类型决定调用哪个方法,而不考虑参数类型。在...

    JVM 方法调用之静态分派(详解)

    "JVM 方法调用之静态分派详解" 静态分派是JVM 方法调用中的一种机制,根据分派依据的宗量数可分为单分派和多分派。静态分派的典型应用是方法重载,发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行...

    基于Java语言的公司任务分派系统设计与实现.pdf

    基于Java语言的公司任务分派系统设计与实现 本文设计并实现了一款基于Java语言的公司任务分派系统,旨在帮助公司更好地管理任务和员工信息。该系统采用MySQL数据库作为后台数据库,使用Java语言编写,具有简单、...

    Corleone:Java注解处理器通过简单的语法分派和连接后台任务

    柯里昂 Java 注释处理器库,用于通过简单的语法以解耦的方式分派和连接后台任务。用法库的使用非常简单。 首先,您需要了解可用的注释。注释@Job :在后台任务类之上使用,它可以包含多个@Rule类型的@Rule 。 @Rule ...

    Java pitfalls图书

    Java中的方法分派包括静态分派和动态分派,这个类可能包含关于多态性或方法重载的示例。 这些类涵盖了Java编程中的多个方面,包括集合操作、持久化、GUI布局、对象序列化、库管理以及测试实践。了解这些知识点对于...

    用Java事件处理机制实现录制回放功能

    5. **事件回放**:在回放阶段,程序需要重新生成并分派已记录的事件,这可能需要重新构造事件对象并调用相应的`postEvent()`方法。 两种实现方式可能包括: 1. **基于模拟的回放**:使用`java.awt.Robot`类模拟...

    深入理解Java虚拟机笔记(带目录).docx

    深入理解 Java 虚拟机笔记 ...Java 中的虚方法表用于存储虚方法的信息,包括方法的名称、描述符和方法体。 Java 内存模型(JMM) Java 内存模型(JMM)是 Java 语言的内存模型,用于定义 Java 程序的内存行为。

Global site tag (gtag.js) - Google Analytics