虚拟机(8)方法调用

方法调用

方法调用不等同与方法执行,方法调用阶段唯一的任务就是确定调用方法的版本。就是调用哪一个方法。不涉及到方法内部运行过程。

解析

方法调用的目标方法在Class文件里面都是一个常量池中的符号引用。

在类加载解析阶段,会将一部分符号引用转化为直接引用,这种解析成立的前提就是:方法在程序执行之前就有一个可确定的调用版本,并且这个调用版本在运行期间是不可以改变的。这类方法调用称为“解析”(Resolution)。

符合上面描述的方法主要是:静态方法,私有方法。

静态方法与类型直接关联,

私有方法外部不可访问。

因此他们都适合在类加载阶段进行解析。

Java虚拟机方法调用字节码指令为:

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

只要能被 invokestatic 和 invokespecial 指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合条件的有  静态方法、私有方法、实例构造器、父类方法 4类,他们在类加载的时候会把符号引用解析为该方法的直接引用,称为“非虚方法”。其他的方法称为“虚方法”(除了final方法)

public class StaticMethod {
    //类方法
    public static void test(){
        System.out.println("test");
    }
    public static void main(String[] args) {
        StaticMethod.test();
    }
}

虽然 final 方法是使用 invokevirtual 指令来调用的,但是由于它无法被覆盖,没有其他版本,所以也无须对方法接收者进行多态选择,或者说选择的结果肯定是唯一的,Java规范中也明确说明了 final 方法是一种非虚方法。

解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会把涉及的符号引用全部变为直接引用,不会在运行期间完成。而分派调用可能是静态的,也可能是动态的,可以分为4中,分别是 静态单分派、静态多分派、动态单分派、动态多分派。

分派

Java是面向对象的语言,具备3个基本特征:继承、封装、多态。

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 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 staticDispatch = new StaticDispatch();
        staticDispatch.sayHello(man);
        staticDispatch.sayHello(woman);
    }
}

执行结果

hello Human
hello Human

Process finished with exit code 0

Human man = new Man();

上面代码中的“Human”称为变量的静态类型(Static Type),或者叫做变量的外观类型(Apparent Type),后面的“Man”则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生变化。区别是:

  • 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期间可知的;
  • 实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么
public static void main(String[] var0) {
        StaticDispatch.Man var1 = new StaticDispatch.Man();
        StaticDispatch.Woman var2 = new StaticDispatch.Woman();
        StaticDispatch var3 = new StaticDispatch();
        var3.sayHello((StaticDispatch.Human)var1);
        var3.sayHello((StaticDispatch.Human)var2);
    }

通过反编译class文件我们看到,实际编译器在编译期间已经把变量变化为“Human”,如果我们想要让静态类型变化,也只能强制转换类型或者在创建对象时就指定实际类型。

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

Process finished with exit code 0

在方法接收者已经确定是对象“staticDispatch”的前提下,使用那个重载版本,就完全取决于传入参数的数量和数据类型。静态类型是编译器可知的,Javac编译器会根据参数的静态类型决定使用哪个重载版本。

    所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载,静态分派发生在编译阶段。

    重载方法匹配优先级

/**
 * 静态分派,方法重载
 */
public class StaticOverLoad {

    public static void sayHello(Object arg){
        System.out.println("hello Object");
    }
    public static void sayHello(int arg){
        System.out.println("hello int");
    }
    public static void sayHello(long arg){
        System.out.println("hello long");
    }
    public static void sayHello(Character arg){
        System.out.println("hello Character");
    }
    public static void sayHello(char arg){
        System.out.println("hello char");
    }
    public static void sayHello(char... arg){
        System.out.println("hello char...");
    }
    public static void sayHello(Serializable arg){
        System.out.println("hello Serializable");
    }
    public static void main(String[] args) {
        StaticOverLoad.sayHello('x');
    }
}

    代码输出 hello char

    这个很好理解 ‘x’是个char类型的数据,如果注释掉sayHello(Object arg),那么输入变成了    hello int

    这时发生了一次自动类型转换,‘x’除了可以表示一个字符串,也可以表示数字120,因此参数类型为int的重载也是合适的,如果继续注释掉sayHello(int arg)方法,那么输出变成了

        hello long

    这里发生了两次自动类型转换,‘x’转型为整数120之后,又转变为了120L,实际自动类型转换能继续发生多次,按照 char -> int -> logn -> float -> double 的顺序转型进行匹配。但是不会匹配到 byte和short类型的重载,因为char到byte或short的转型是不安全的。

   为什么不安全

    The range of the char type is 0 to 2^16 - 1 (0 to 65535).

    The short range is -2^15 to 2^15 - 1 (−32,768 to 32,767).

    因为char类型的范围是 0 到 2的16次方减1,就是0-65535

    而short类型的范围则是 −32,768 to 32,767

    如果高位char类型转型成short会有丢失精度的问题。

    继续注释掉sayHello(loan arg),那输出会变为

    hello Character

    这时是发生了一次自动装箱,‘x’被包装为它的封装类型 java.lang.Character,所以匹配到了参数类型为Character的重载,继续注释掉sayHello(Character arg),那么输出会变为:

    hello Serializable

    输出Serializable,是因为java.lang.Serializable是java.lang.Character类实现的一个接口,当自动装箱之后发现还是找不到装箱类,但是找到了装箱类实现了的接口类型,所以紧接着又发生一次自动转型,char可以转型成int,但是Character是绝对不会转型为Integer的,它只能安全的转型为它的实现的接口或父类。Character还实现了另外一个接口java.lang.Comparable<Character>,如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,那么他们此时的优先级是一样的,编译器无法确定要自动转型为哪种类型,拒绝编译,程序必须显示的指定字面量的静态类型,如:

sayHello((Comparable<Character>)'x')

才能编译通过。

    继续注释掉 sayHello(Serializable arg),输出将会变为 

    hello object

    这时是char装箱后转型为父类,如果有多个父类,那将在继承关系中从下王上开始搜索,越接近上层的优先级越低。即使方法调用传入的参数值为null时,这个规则仍然适用。如果我们把sayHello(object arg)也注释掉,输出将会变为

    hello char. . .

    7个重载方法已经被注释的只剩一个了,可见变长参数的重载优先级最低。

2.动态分派

    动态分派,是多态性的另外一个重要体现“重写”有着密切的关系。

public class DynamicDispatch {
    static abstract 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

Process finished with exit code 0

通过javap命令输出字节码

17: invokevirtual #6                  // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V
        24: new           #4                  // class com/pintec/javaDynamic/DynamicDispatch$Woman
        27: dup
        28: invokespecial #5                  // Method com/pintec/javaDynamic/DynamicDispatch$Woman."<init>":()V
        31: astore_1
        32: aload_1
        33: invokevirtual #6                  // Method com/pintec/javaDynamic/DynamicDispatch$Human.sayHello:()V

可以看到调用sayHello方法的指令都是invokevirtual。

invokevirtual指令大致可以分为一下几个步骤:

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

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

 

3.单分派与多分派

    

public class Dispatch {

    static class QQ{}
    static class _360{}
    public static class Father{
        public void choice(QQ qq){
            System.out.println("father choice qq");
        }
        public void choice(_360 qq){
            System.out.println("father choice _360");
        }
    }
    public static class Son extends Father{
        @Override
        public void choice(QQ qq){
            System.out.println("Son choice qq");
        }
        @Override
        public void choice(_360 qq){
            System.out.println("Son choice _360");
        }
    }
    public static void main(String[] args) {
        Father father = new Father();
        Father son = new Son();
        father.choice(new _360());
        son.choice(new QQ());
    }
}
father choice _360
Son choice qq

Process finished with exit code 0
wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

    先看编译阶段的选择过程,也就是静态分派的过程,这时选择目标方法的依据有两点:

  1. 静态类型是Father还是Son
  2. 方法参数是QQ还是360

    这次选择结果最终是产生了量条invokevirtual指令,两条指令的参数分别为常量池中指向

Father.choice(360)和Father.choice(qq)方法的符号引用,因为根据连个宗量进行选择,所以java语言的静态分派属于多分派类型。

    在看运行阶段虚拟机的选择,也就是动态分派的过程,在执行son.choice(new QQ())这句代码时,更准确地说,是在执行这句代码所对应的invokevirtual指令时,由于编译器已经决定目标方法的签名必须为choice(QQ),此时不关心传递过来的参数是什么,因为参数是静态类型、实际类型都对方法的选择不会构成任何影响,唯一可以影响虚拟机选择的因素只有此方法的接收者的实际类型是Father还是Son。因为只有一个宗量作为选择依据,所以Java语言的动态分派属于单分派类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值