Java中派怎么打出来,Java中的方法分派

Java中的方法分派

程序设计中,许多时候把不同的函数命名成名字相同可以更清晰地表达出语义。相同的方法名字,需要根据方法的参数、调用者等信息来确定到底应该执行哪个方法。这个确定执行哪个方法的过程就是方法分派。

众所周知,Java是一门面向对象语言,为我们提供了重载和重写的机制。那么,在重载和重写的背后是一个怎样的原理或者原则呢?今天通过实验来揭示

一,静态分派

假设,有如下的几个类和方法,并且接下来的一系列实验也都基于这些类和方法:

public void sayHello(Human human){

System.out.println("human say hello!");

}

public void sayHello(Man man){

System.out.println("man say hello.");

}

private static class Human{

public void sayHello(){

System.out.println("say hello in human");

}

public void eat(Fruit fruit){

System.out.println("human eat fruit.");

}

public void eat(Apple apple){

System.out.println("human eat apple.");

}

}

private static class Man extends Human{

public void sayHello(){

System.out.println("say hello in man");

}

public void eat(Fruit fruit) {

System.out.println("man eat fruit.");

}

public void eat(Apple apple) {

System.out.println("man eat apple.");

}

}

private static class Fruit{}

private static class Apple extends Fruit{}

来看一个简单的静态分派的例子

public void overload(){

Human human = new Human();

Human man = new Man();

Man realMan = new Man();

sayHello(human);

sayHello(man);

sayHello(realMan);

}

这段代码唯一不太确定的就是sayHello(man)到底调用哪个目标方法,man是一个子类的类型,但却是一个父类类型的引用。最终虚拟机选择的是sayHello(Human)方法,也就是说,在静态分派阶段,虚拟机是根据引用的类型来判断调用哪个重载方法的。实际上,静态分派是在编译期完成的,调用哪个重载方法在编译期就已经确定,如刚刚的sayHello(man)语句,经过编译器的编译会把sayHello(Human)做为调用目标写入到invokevirtual指令的参数中。

静态分派还有一个特点,如果编译期没有找到确切的重载方法,则会尝试找到一个更加合适的版本。如刚才的例子中,把sayHello(Man)方法注释掉,sayHello(realMan)还是可以成功执行,只不过此时执行的目标方法为sayHello(Human)。也就是说,编译期找到了参数为Man的父类的重载方法。但是,若一个类型父类或实现的接口不止一个,而当前类中又分别有以其父类活接口类型作为参数的重载方法,编译期会默认选择其中一个吗?当然不会,这样就会出现语义上的歧义,编译器的选择是报出错误,编译不通过。如下面的例子:

public class AmbiguousDispatch {

private interface Eater{}

private interface Drinker{}

//同时继承子类和实现接口是一样的,也会报错

private class Human implements Eater,Drinker{}

public void serve(Eater eater){}

public void serve(Drinker drinker){}

@Test

public void ambiguous(){

Human human = new Human();

//serve(human);

}

}

如果代码中注释的serve(human)放开的话,编译器会提示Ambiguous Method Call。因为它不知道该调用哪个方法,不知道这个人是按吃货算还是按酒鬼算~

二,动态分派

方法的动态分派发生在执行阶段,虚拟机根据执行方法的实际类型来判断执行哪个目标方法。

public void dynamicDispatch(){

Human man = new Man();

Human human = new Human();

Man realMan = new Man();

human.sayHello();

man.sayHello();

realMan.sayHello();

}

对于任何一个Java程序员,都会准确的推断出这段代码的结果。父类和子类都实现了相同的方法,在编译期不能确定执行父类中的方法还是执行子类中复写的方法,而需要到运行期知道了执行方法的具体类型才会判断出目标方法。如man.sayHello(),man是一个Human类型的引用,它的实际类型是Man而且可能会发生变化,到了运行期已经可以确切的知道了它的实际类型就是Man,所以会期限Man中复写的sayHello方法。

如果,方法的调用过程既有静态分派又有动态分派,则会按顺序先进行静态分派,然后再进行动态分派。

三,静态方法

在Java中,静态方法同样也可以继承,可以重载。那么它是怎么分派的呢?先看下面的实验

public class StaticMethod { private static class Human{

public static void sayHello(){

System.out.println("human say hello.");

}

}

private static class Man extends Human{

public static void sayHello(){System.out.println("man say hello");}

//public void sayHello(){}

}

@Test

public void callMethod(){

Human.sayHello();

Man.sayHello();

Human human = new Human();

Human man = new Human();

Man realMan = new Man();

human.sayHello();

man.sayHello();

realMan.sayHello();

}

}

静态方法可以通过类名直接访问,子类会继承父类的静态方法,但是不会复写父类方法。如果子类声明了和父类一样的方法,则这时候发生的是隐藏,而不是复写。因为静态方法是属于类的,即使是通过类的实例调用,最终生成的字节码指令也是invokestatic,目标方法是在编译期就已经确定好的。同时子类的实例方法也不能复写父类中的静态方法,编译器会报错。上面例子的结果为:

human say hello.

man say hello

human say hello.

human say hello.

man say hello

四,虚方法表

大多数的虚拟机动态分派时,都会利用虚方法表查找目标方法。我们知道,子类会继承父类中的方法,还有可能复写其中的方法。虚方法表中存放的就是当前类中所有方法的实际入口地址。如果方法直接继承自父类则子类中的方法入口地址与父类的相同,都是父类中方法的入口地址。如果子类复写了父类中的方法,则它的虚方法表中的入口地址就是自己复写后的方法的入口地址。运行阶段,虚拟机只需要查找实际类型中的虚拟方法表,就可以得到最终的目标方法入口地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值