方法调用

方法调用并不等于方法的执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即哪一个方法),暂时还不涉及到方法内部的具体运行过程。在程序运行时,进行方法调用是最被普遍,最频繁的操作。

解析

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

这类方法主要包括:静态方法和私有方法两大类,前者与类直接关联,后者在外部不可被访问,这两种方法各自的特点决定了他们都不肯能通过继承或者别的方式重写其他版本,因此他们都适合在类的加载阶段进行解析。在Java虚拟机里提供了5调方法调用字节码指令:

  • invokestatic:调用静态方法。
  • invokespecial:调用实例构造器< init>方法,私有方法和父类方法。
  • invokevirtual:调用所有的虚方法。
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
  • invokedynamic:现在运行时动态解析调用点限定符所引用的方法,然后再执行该方法。

只要是能被invokestatic和jinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器,父类方法4类,他们载类加载的时候就回把符号引用解析为该方法的直接引用。这些都可以称为非虚方法,与之对应的就是虚方法。

Java种的废墟方法除了使用invokestatic,invokespecial调用的方法之外还有一种,就是被final修饰的方法。再Java语言规范中明确说明了final方法是一种非虚方法。

分派

解析调用是一个静态的过程,在编译期间就完全确定,不会延迟到运行期再去完成。而分派调用则可能是静态的也可能是动态的。根据分派的宗量数可分为单分派和多分派。这两类分派又可两两组合成:静态单分派,静态多分派,动态单分派和动态多分派4中分派组合。

分派体现了Java的多态性,如“重载”和“重写”。

静态分派

我们来看一段代码:

//静态分派
public class StaticDispatch {

    static abstract class Human{

    }

    static class Man extends Human{

    }

    static class Woman extends Human{

    }

    public void sayhello(Human guy){
        System.out.println("hello,Human");
    }

    public void sayhello(Man guy){
        System.out.println("hello,Man");
    }

    public void sayhello(Woman guy){
        System.out.println("hello,Woman");
    }


    public static void main(String[] args){
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch sr = new StaticDispatch();
        sr.sayhello(man);
        sr.sayhello(woman);
    }

}

结果是什么呢?

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

在上面的代码中,使用哪个重载方法完全取决于传入的参数的数据类型和数量。代码中刻意的定义了两个静态类型相同但实际类型不同的变量,但虚拟机在重载时是通过参数的静态类型而不是实际类型作为判断依据的。并且静态类型是编译期可知的,因此,在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本,在此,我们就能知道,编译器最终会选择sayHello(Human)方法作为调用目标,并把这个方法的符号引用西岛main()方法里的两条invokevirtual指令参数中。

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

动态分派

动态分派是多态性的另一个重要体现:重写。还是以上面的示例为例。如果man和woman分别调用sayHello()方法时执行的结果是什么呢?

动态多态需要用到invokevirtual指令,invokevirtual指令的运行时解析过程大致分为以下步骤:

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

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

单分派与多分派

方法的接收者与方法的参数统称为方法的宗量。根据分派基于多少种宗量,可以将分派划分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派则是根据多于一个宗量对目标方法进行选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值