Java方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调 用哪一个方法),暂时不涉及方法内部的具体执行过程。Class 文件的编译过程不包含传统编译中的连接步骤,一切方法调用在 Class 文件中存储的都是符号引用,而不是方法在实际运行时内存布局中的入口地址。这个特性使得 Java 方法调用过程变得复杂,某些需要在类加载器件,甚至到运行期间才能确定目标方法的直接引用。

1、 解析

符号引用能转为直接引用成立的前提是:方法在程序真正运行之前就有一个可确定的调用版 本,并且这个方法的调用版本在运行期是不可改变的。换句话说,调用目标在程序代码写好、 编译器进行编译时就必须确定下来。这类方法的调用称为解析。

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

调用不同类型的方法,字节码指令集里设计了不同的指令。Java虚拟机支持以下5种方法调用字节码指令:

  1. invokestatic:调用静态方法;
  2. invokespecial:用于调用实例构造<init>()方法、私有方法和父类中的方法;
  3. invokevirtual:用于调用所有的虚方法;
  4. invokeinterface:用于调用接口方法,会在运行时再确定一个实现该接口的对象;
  5. invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后执行该方法。前面4条调用指令,分派逻辑都固化在Java虚拟机内部,而invokedynamic指令分派逻辑是用户设定的引导方法来决定的。

 只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、私有方法、实例构造器、父类方法 4 类,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这5种方法调用会在类加载的时候就会把符号引用解析为该方法的直接引用。这个方法可以称为非虚方法,与之相反, 其他方法称为虚方法。

非虚方法也包含被 final 修饰的方法。虽然 final 方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法调用者进行多态选择,又或者说多态选择的结果肯定是唯一的。

解析调用一定是个静态过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转为可确定的直接引用,不会延迟到运行期再去完成。而分派调用则可能是静态 的也可能是动态的,根据分派依据的宗量数可分为单分派和多分派。这两类分派方式的组合就构成了静态单分派、静态多分派、动态单分派、动态多分派这 4 中分派组合情况。

2、分派

分派调用过程将会揭示多态性的一些最基本体现,如重载和重写。

2.1、静态分派

        

//方法静态分派演示

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,guy!");
    }

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

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

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

运行结果:

hello,guy!
hello,guy!

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

代码中刻意定义了两个静态类型相同但是实际类型不同的变量,但编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译器可知的,因此,在编译阶段,Javac 编译器会根据参数的静态类型决定使用哪个重载版本,所以选择了 sayHello (Human)作为调用目标,并把这个方法的符号引用写到 main()方法里的两条 invokevirtual 指令的参数中。

所有依赖静态类型来定位方法执行版本的分派动作被称为静态分派。静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。另外,编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是唯一 的,往往只能确定一个更加合适的版本。产生这种模糊结论的主要原因是字面量不需要定义, 所以字面量没有显式地静态类型,它的静态类型只能通过语言上的规则去理解和推断。

2.2、动态分派

//方法动态分派演示
public class DynamicDispatch{
    
    static abstarct class Human{
        protected abstract void sayHello();
    }

    static class Man extends Human{
        @Override
        protected void sayHello(){
            System.out.println("man say hello");
        }
    }

    static class Woman extends Human{
        @Override
        protected void sayHello(){
            System.out.println("woman say hello");
        }
    }

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

运行结果:

man say hello
woman say hello
woman say hello

导致整个现象的原因很明显,是这两个变量的实际类型不同。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值