Java静态分派与动态分派(二)

方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。

在程序运行时,进行方法调用是最普遍、最频繁的操作,但是Class文件的编译过程不包括传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相对于之前说的直接引用)。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

解析

所有方法调用中的目标方法在Class文件里面都是一个常量池中的引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。这种解析能成立的前提是:方法在程序真正执行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来,这类方法的调用称为解析

在Java语言中符合“编译期可知,运行期不可变”这个要求的方法,主要包括静态方法私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们不可能通过继承或别的方式重写其他版本,因此他们适合在类加载阶段进行解析。

静态方法私有方法实例构造器父类方法。这些方法称为非虚方法,它们在类加载的时候就会把符号引用解析为该方法的直接引用。与之相反,其他方法称为虚方法(除去final方法)。

分派

静态分派

[java] view plain copy

  1. public class StaticDispatch {
  2. static abstract class Human{
  3. }
  4. static class Man extends Human{
  5. }
  6. static class Woman extends Human{
  7. }
  8. public static void sayHello(Human guy){
  9. System.out.println("hello,guy!");
  10. }
  11. public static void sayHello(Man guy){
  12. System.out.println("hello,gentlemen!");
  13. }
  14. public static void sayHello(Woman guy){
  15. System.out.println("hello,lady!");
  16. }
  17. public static void main(String[] args) {
  18. Human man=new Man();
  19. Human woman=new Woman();
  20. sayHello(man);
  21. sayHello(woman);
  22. }
  23. }

输出:

hello,guy!
hello,guy!

Human man=new Man();
我们把“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型,静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型在编译器可知;而实际类型变化的结果在运行期才确定,编译器在编译期并不知道一个对象的实际类型是什么。

[java] view plain copy

  1. Human man=new Man();
  2. sayHello(man);
  3. sayHello((Man)man);//类型转换,静态类型变化,我们知道转型后的静态类型一定是Man
  4. man=new Woman(); //实际类型变化,实际类型却是不确定的
  5. sayHello(man);
  6. sayHello((Woman)man);//类型转换,静态类型变化

输出:

hello,guy!
hello,gentlemen!
hello,guy!
hello,lady!

编译器在重载时是通过参数的静态类型而不是实际类型作为判定的依据。并且静态类型在编译期可知,因此,编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用就是方法重载

静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,而是由编译器来完成。

但是,字面量没有显示的静态类型,它的静态类型只能通过语言上的规则去理解和推断。

[java] view plain copy

  1. public class LiteralTest {
  2. /**/
  3. public static void sayHello(char arg){
  4. System.out.println("hello char");
  5. }
  6. public static void sayHello(int arg){
  7. System.out.println("hello int");
  8. }
  9. public static void sayHello(long arg){
  10. System.out.println("hello long");
  11. }
  12. public static void sayHello(Character arg){
  13. System.out.println("hello Character");
  14. }
  15. public static void main(String[] args) {
  16. sayHello(‘a‘);
  17. }
  18. }

输出:

hello char
将重载方法从上向下依次注释,将会得到不同的输出。

如果编译器无法确定要自定转型为哪种类型,会提示类型模糊,拒绝编译。

[java] view plain copy

  1. import java.util.Random;
  2. public class LiteralTest {
  3. /**/
  4. public static void sayHello(String arg){//新增重载方法
  5. System.out.println("hello String");
  6. }
  7. public static void sayHello(char arg){
  8. System.out.println("hello char");
  9. }
  10. public static void sayHello(int arg){
  11. System.out.println("hello int");
  12. }
  13. public static void sayHello(long arg){
  14. System.out.println("hello long");
  15. }
  16. public static void sayHello(Character arg){
  17. System.out.println("hello Character");
  18. }
  19. public static void main(String[] args) {
  20. Random r=new Random();
  21. String s="abc";
  22. int i=0;
  23. sayHello(r.nextInt()%2!=0?s:i);//编译错误
  24. sayHello(r.nextInt()%2!=0?‘a‘:false);//编译错误
  25. }
  26. }

动态分派

[java] view plain copy

  1. public class DynamicDispatch {
  2. static abstract class Human{
  3. protected abstract void sayHello();
  4. }
  5. static class Man extends Human{
  6. @Override
  7. protected void sayHello() {
  8. System.out.println("man say hello!");
  9. }
  10. }
  11. static class Woman extends Human{
  12. @Override
  13. protected void sayHello() {
  14. System.out.println("woman say hello!");
  15. }
  16. }
  17. public static void main(String[] args) {
  18. Human man=new Man();
  19. Human woman=new Woman();
  20. man.sayHello();
  21. woman.sayHello();
  22. man=new Woman();
  23. man.sayHello();
  24. }
  25. }

输出:
man say hello!
woman say hello!
woman say hello!

显然,这里不可能再根据静态类型来决定,因为静态类型同样是Human的两个变量man和woman在调用sayHello()方法时执行了不同的行为,并且变量man在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同,Java虚拟机是如何根据实际类型来分派方法执行版本的呢?
我们从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:

1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。

由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同直接引用上,这个过程就是Java语言方法重写的本质。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

虚拟机动态分派的实现

前面介绍的分派过程,作为对虚拟机概念模型的解析基本上已经足够了,它已经解决了虚拟机在分派中"会做什么"这个问题。

但是,虚拟机”具体是如何做到的“,可能各种虚拟机实现都会有些差别。

由于动态分派是非常频繁的动作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,因此虚拟机的实际实现中基于性能的考虑,大部分实现都不会真正的进行如此频繁的搜索。面对这种情况,最常用的”稳定优化“手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称为vtable),使用虚方法表索引代替元数据查找以提高性能。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都是指向父类的实际入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实际版本的入口地址。

为了程序实现上的方便,具有相同签名的方法,在父类、子类的虚方法表中具有一样的索引序号,这样当类型变换时,仅仅需要变更查找的方法表,就可以从不同的虚方法表中按索引转换出所需要的入口地址。

方法表一般在类加载阶段的连接阶段进行初始化,准备了类的变量初始值后,虚拟机会把该类的方法表也初始化完毕。

内容源自:

《深入理解Java虚拟机》

转自:http://blog.csdn.net/sunxianghuang/article/details/52280002

时间: 2024-08-29 06:07:06

Java静态分派与动态分派(二)的相关文章

Java静态分派和动态分派

前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new Son(); //静态分派 print(father); } void print(Father father) { System.out.println("this is father"); } void print(Son son) { System.out.println("

(三十)分派调用:静态分派和动态分派

分派调用 其实分派分为两种,即动态分派和静态分派.我们在了解分派的时候,通常把它们与重写和重载结合到一起. 重载(overload)与静态分派 我们先看一个题: public class Main { static abstract class Father { } static class Son extends Father { } static class Daughter extends Father { } public void getSex(Daughter daughter) {

Java方法重载与重写(静态分派与动态分派)

Java面向对象3个基本特征:继承.封装和多态:多态主要体现在重载和重写: 1.静态分派 静态分派与重载有关,虚拟机在重载时是通过参数的静态类型,而不是运行时的实际类型作为判定依据的:静态类型在编译期是可知的: 1)基本类型 以char为例,按照char>int>long>double>float>double>Character>Serializable>Object>...(变长参数,将其视为一个数组元素) 变长参数的重载优先级最低 (注意char

多态性实现机制——静态分派与动态分派

方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给 Java 带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析.这在前面的“Java 内存区域与内存溢出”一文中有提到. 静态解析成立的前提是:方法在程序真正执行前就有一个可确定的调用版本,并

Java静态代理与动态代理模式的实现

前言:    在现实生活中,考虑以下的场景:小王打算要去租房,他相中了一个房子,准备去找房东洽谈相关事宜.但是房东他很忙,平时上班没时间,总没有时间见面,他也没办法.后来,房东想了一个办法,他找到了一个人代替自己和小王洽谈,房东本人不用出面,他只要把他的对房客的要求告诉他找的那个人,那个人和你商量就可以了,这样就可以完成租房这件事了.这种现实场景比比皆是,所呈现出来的其实就是代理模式的原型的一种.我们把焦点转向编程,你是否在编程中经常遇见这样一个问题,对于访问某个对象,我们希望给它的方法前加入一

Java 静态代理和动态代理

Java 静态代理 静态代理通常用于对原有业务逻辑的扩充.比如持有二方包的某个类,并调用了其中的某些方法.然后出于某种原因,比如记录日志.打印方法执行时间,但是又不好将这些逻辑写入二方包的方法里.所以可以创建一个代理类实现和二方方法相同的方法,通过让代理类持有真实对象,然后在原代码中调用代理类方法,来达到添加我们需要业务逻辑的目的. 这其实也就是代理模式的一种实现,通过对真实对象的封装,来实现扩展性. 一个典型的代理模式通常有三个角色,这里称之为**代理三要素** 共同接口 public int

java静态代理和动态代理(一)

代理Proxy: Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题. 代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问.代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理. 为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别.通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从

深入浅出java静态代理和动态代理

首先介绍一下,什么是代理: 代理模式,是常用的设计模式.特征是,代理类与委托类有相同的接口,代理类主要负责为委托类预处理消息.过滤消息.把消息转发给委托类.以及事后处理消息. 代理类和委托类,存在着关联关系.代理类的对象本身并不真正实现服务,知识通过调用委托类的对象的相关方法. 代理类可以分为两种:静态代理和动态代理. 静态代理: 代理类是由程序员创建,或由工具生成的代码 编译成的.在程序运行前,代理类的 *.class文件已经存在了.直接就可以运行 . 动态代理: 动态代理的代理类.没有直接由

java静态代理与动态代理简单分析

原创作品,可以转载,但是请标注出处地址http://www.cnblogs.com/V1haoge/p/5860749.html 1.动态代理(Dynamic Proxy) 代理分为静态代理和动态代理,静态代理是在编译时就将接口.实现类.代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能. 其实方法直接调用就可以完成功能,为什么