Java中的方法分派
2016-01-02 14:32
441 查看
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
四,虚方法表
大多数的虚拟机动态分派时,都会利用虚方法表查找目标方法。我们知道,子类会继承父类中的方法,还有可能复写其中的方法。虚方法表中存放的就是当前类中所有方法的实际入口地址。如果方法直接继承自父类则子类中的方法入口地址与父类的相同,都是父类中方法的入口地址。如果子类复写了父类中的方法,则它的虚方法表中的入口地址就是自己复写后的方法的入口地址。运行阶段,虚拟机只需要查找实际类型中的虚拟方法表,就可以得到最终的目标方法入口地址。
相关文章推荐
- Java8中 局部内部类访问的局部变量不必用final修饰
- JAVA中堆和栈的区别
- java的增、删、改、查与及批量增加
- 棋盘覆盖 算法分析、设计与实现(Java)
- JDK与JRE的区别
- JAVA EE 7 SDK Tutorial分析
- Spring整合Shiro做权限控制模块详细案例分析
- JUC之ReentrantReadWriteLock(JDK1.8源码)
- IO_缓冲流_转换流_字节转为字符_乱码分析_编码与解码JAVA152-154
- Android Settings 导入eclipse
- java并发API: Semaphore管理资源许可
- JAVA WEB学习笔记(一):JDK的安装及环境变量的配置
- java面向对象编程
- eclipse 替换 查找 特定字段
- Spring 学习笔记 2. 尚硅谷_佟刚_Spring_IOC&DI概述
- 如何获取Java层的函数在Dalvik中对应的Method?
- springmvc系列之一(原理)
- java设计模式
- Java反射机制详解:从classLoader到反射机制再到抽象工厂设计模式
- 从iOS学习Java(1)