`
Ydoing
  • 浏览: 106181 次
  • 性别: 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高级特性与最佳实践>

Java代码收藏代码
  1. publicclassStaticDispatch{
  2. staticabstractclassHuman{
  3. }
  4. staticclassManextendsHuman{
  5. }
  6. staticclassWomanextendsHuman{
  7. }
  8. publicvoidsayHello(Humanhuman){
  9. System.out.println("humansayhello");
  10. }
  11. publicvoidsayHello(Manman){
  12. System.out.println("mansayhello");
  13. }
  14. publicvoidsayHello(Womanwoman){
  15. System.out.println("womansayhello");
  16. }
  17. /**
  18. *@paramargs
  19. */
  20. publicstaticvoidmain(String[]args){
  21. Humanman=newMan();
  22. Humanwoman=newWoman();
  23. StaticDispatchsd=newStaticDispatch();
  24. sd.sayHello(man);
  25. sd.sayHello(woman);
  26. }
  27. }

最后的输出是

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. publicclassDynamicDispatch{
  2. staticabstractclassHuman{
  3. protectedabstractvoidsayHello();
  4. }
  5. staticclassManextendsHuman{
  6. @Override
  7. protectedvoidsayHello(){
  8. System.out.println("mansayhello");
  9. }
  10. }
  11. staticclassWomanextendsHuman{
  12. @Override
  13. protectedvoidsayHello(){
  14. System.out.println("womansayhello");
  15. }
  16. }
  17. /**
  18. *@paramargs
  19. */
  20. publicstaticvoidmain(String[]args){
  21. Humanman=newMan();
  22. Humanwoman=newWoman();
  23. man.sayHello();
  24. woman.sayHello();
  25. }
  26. }

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. publicclassSuperTest{
  2. publicstaticvoidmain(String[]args){
  3. newSub().exampleMethod();
  4. }
  5. }
  6. classSuper{
  7. <spanstyle="color:#ff0000;">private</span>voidinterestingMethod(){
  8. System.out.println("Super'sinterestingMethod");
  9. }
  10. voidexampleMethod(){
  11. interestingMethod();
  12. }
  13. }
  14. classSubextendsSuper{
  15. voidinterestingMethod(){
  16. System.out.println("Sub'sinterestingMethod");
  17. }
  18. }

console输出
Super'sinterestingMethod

代码2

Java代码收藏代码
  1. publicclassSuperTest{
  2. publicstaticvoidmain(String[]args){
  3. newSub().exampleMethod();
  4. }
  5. }
  6. classSuper{
  7. voidinterestingMethod(){
  8. System.out.println("Super'sinterestingMethod");
  9. }
  10. voidexampleMethod(){
  11. interestingMethod();
  12. }
  13. }
  14. classSubextendsSuper{
  15. voidinterestingMethod(){
  16. System.out.println("Sub'sinterestingMethod");
  17. }
  18. }

console输出 写道
Sub'sinterestingMethod

代码一与代码二,只有一个区别,就是在代码一中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. publicclassA{
  2. publicvoidfoo(){/*...*/}
  3. }


B.java
Java代码收藏代码
  1. publicclassBextendsA{
  2. publicvoidfoo(){/*...*/}
  3. }


C.java
Java代码收藏代码
  1. publicclassCextendsB{
  2. publicfinalvoidfoo(){/*...*/}
  3. }


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

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

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

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

4 单分派与多分派

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

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

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

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

Java代码收藏代码
  1. publicclassDispatcher{
  2. staticclassQQ{
  3. }
  4. staticclass_360{
  5. }
  6. publicstaticclassFather{
  7. publicvoidhardChoice(QQqq){
  8. System.out.println("fatherchooseqq");
  9. }
  10. publicvoidhardChoice(_360_360){
  11. System.out.println("fatherchoose360");
  12. }
  13. }
  14. publicstaticclassSonextendsFather{
  15. publicvoidhardChoice(QQqq){
  16. System.out.println("sonchooseqq");
  17. }
  18. publicvoidhardChoice(_360_360){
  19. System.out.println("sonchoose360");
  20. }
  21. }
  22. publicstaticvoidmain(String[]args){
  23. Fatherfather=newFather();
  24. Fatherson=newSon();
  25. father.hardChoice(new_360());
  26. son.hardChoice(newQQ());
  27. }
  28. }

console输出 写道
father choose 360
son choose qq

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

Java代码收藏代码
  1. father.hardChoice(new_360());
  2. son.hardChoice(newQQ());

我们分别从编译阶段和运行阶段分别分析这个分派的过程。在编译阶段,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. Humanman=newMan();
  2. Humanwoman=newWoman();
  3. StaticDispatchsd=newStaticDispatch();
  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的动态分派和静态分派的实现

    在替换之前,由于 Java 的方法重写、重载,就导致符号引用对应的方法可能是一个虚方法,那么方法的真实实现在运行时就可能有多个。所以,在将符号引用替换为真实地址时,还需要做一件事情:那就是确定符号引用要替换...

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

    动态分派则是在运行时根据实际对象的类型来确定调用哪个方法,它是多态性的重要体现,主要体现在方法的重写(Overriding)上。例如: ```java class Parent { void printType() { System.out.println("Parent"); }...

    Java经典问题答案(带书签)

    - 重载和重写的区别:重载是同一个类中多个同名方法有不同的参数列表,而重写是子类对父类方法的再实现。 - 匿名内部类中对接口的方法为什么要加public:在接口中定义的所有方法默认都是public的,因此匿名内部类...

    java经典面试题

    - 通过方法覆盖(重写)和方法重载来实现。 - 在运行时根据对象的实际类型来确定调用哪个方法。 21. **实现多线程的两种方法**: - 继承 `Thread` 类。 - 实现 `Runnable` 接口。 22. **线程同步的方法**: -...

    从虚拟机指令执行的角度分析JAVA中多态的实现原理.docx

    在这个过程中,方法的重载(Overload)和重写(Override)是两个关键的概念,它们与方法的分派密切相关。 1. **方法的重载(Overload)**: 重载发生在同一类中,通过改变方法的参数列表(参数类型、数量或顺序)...

    2017Java常考面试题 下载

    #### 方法覆盖与重载 - **方法重载**:在同一类中定义多个同名方法,但参数列表不同。编译器根据参数类型和数量选择合适的方法。 - **方法覆盖**:子类重新定义父类的方法,方法签名(包括返回类型、名称和参数列表...

    JVM调用Lambda的表达式实现方法原理详解.docx

    - 动态分派:在运行时根据对象的实际类型(动态类型)确定调用哪个方法,这与多态性紧密相关,即方法重写。比如,`Virus`的子类`Cold`和`CoronaVirus`都重写了`ill`方法,当通过父类引用调用`ill`时,实际调用的是...

    访问者模式商量篇:java的动态绑定与双分派_.docx

    例如,`Father`、`Son1`和`Son2`三个类中,`Father`有一个`method()`方法,`Son1`和`Son2`各自重写了这个方法。当使用`Father`类型的引用`s1`和`s2`来调用`method()`时,实际调用的是`Son1`和`Son2`中的方法,这正是...

    16.方法调用1

    在静态分派中,如Java的多态方法重载,调用版本在编译时就已经确定。而在动态分派中,如方法覆写,调用版本取决于运行时对象的实际类型,这体现了Java的多态性特点。 例如: ```java class Human {} class Man ...

    java需要掌握的技术.docx

    - **静态属性与方法**:理解静态成员的生命周期和使用场景,以及它们与类的关系。 - **静态类与静态代码块**:学习静态类的定义,以及静态代码块的初始化时机。 9. **基础知识点**: - 散落在Java语言中的其他...

    java就业培训教程(个人整理)

    - **实现方式**:方法重写、接口实现。 - **4.4 异常**: - **类型**:检查型异常、运行时异常。 - **处理**:try-catch-finally、throw、throws。 - **4.5 包**: - **组织结构**:逻辑组织类和接口。 - **...

    JAVA 面试 问题和答案

    2.6 Java中的方法重载(Overloading)与重写(Overriding)是什么?重载是指在同一个类中可以存在多个同名方法,只要它们的参数列表不同。重写是指子类对父类方法的覆盖。 2.7 构造器(Constructors)的作用是什么?构造器...

    2014年Java最全面试题以及答案.

    Java中实现多态的机制主要依赖于继承和接口,多态性表现为方法的重载和重写。 35. 垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收? 垃圾回收器的基本原理是...

    JAVA中类的多态的理解.pdf

    1. 方法重载(Overloading)与方法重写(Overriding): 在Java中,可以在同一个类中或不同的类中定义相同名称的方法,这称为方法重载。但多态性更倾向于方法重写,即子类对父类方法的重新定义。文件中提到的`Person...

    葵花宝典史上最全java面试题.doc

    Java中多态分为编译时多态(方法重载)和运行时多态(方法重写)。 2. 基本数据类型与引用类型: - Java的原始数据类型包括int、byte等,它们直接存储值。而Integer等封装类是对象,存储的是对象的引用。原始类型...

    dynamicDispatch:java虚拟机的动态分派的例子程序的说明

    首先,理解动态分派的关键在于了解方法的重写(Override)和重载(Overload)。重写是指子类重新定义父类中的某个非私有方法,具有相同的方法名、返回类型和参数列表,但可能有不同的实现。而重载则是指在同一个类中...

    2022年JAVA基础考试题及答案.docx

    - 另一种方式是直接继承`Thread`类,并重写`run()`方法。这种方式同样需要调用`start()`方法来启动线程。 **例题分析**: - 选项A正确,描述了线程的基本概念。 - 选项B错误,实现了`Runnable`接口的类不能直接调用...

    编程语言java多态性.pdf

    与方法覆盖不同,方法重载是指在同一个类中存在多个同名方法,但它们的参数列表不同(参数个数不同、参数类型不同、参数顺序不同)。重载方法提供了在同一类中实现多个功能相同但参数不同的方法的能力。 4. 抽象类...

Global site tag (gtag.js) - Google Analytics