美文网首页
虚拟机字节码执行引擎【方法调用(二)分派之静态分派】

虚拟机字节码执行引擎【方法调用(二)分派之静态分派】

作者: 云芈山人 | 来源:发表于2021-08-11 23:52 被阅读0次

Java具有面向对象的3个基本特征:继承、封装和多态。分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在Java虚拟机之中是如何实现的,这里的实现指的是虚拟机如何确定正确的目标方法。

静态分派

“分派”(Dispatch)这个词本身具有动态性,一般不应出现在静态语境之中。

为解释静态分派和重载(Overload),以下一段经常出现在面试题中的程序代码。

package com.test;

/**
 * 方法静态变量分派演示
 * @author huyl
 *
 */
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的重载版本?在解决这个问题之前需先了解两个概念。

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

举个例子:

//实际类型变化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();

//静态类型变化
sr.sayHello((Man)human );
sr.sayHello((Woman)human );

对象human的实际类型是可变的,编译期间它完全是个“薛定谔的人”,到底是Man还是Woman,必须等到程序运行到这行的时候才能确定。而human的静态类型是Human,也可以在使用时(如sayHello()方法中的强制转型)临时改变这个类型,但这个改变是在编译期可知的,两次sayHello方法的调用,在编译期完全可以明确转型的是Man还是Woman。

了解了静态类型和实际类型的概念,对以上代码中:main()里面的两次sayHello()方法调用,在方法接收者已经明确是对象“sr”的前提下,使用哪个重载版本,就完全取决于传入参数的数量和数据类型。代码中故意定义了两个静态类型相同,而实际类型不同的变量,但虚拟机(或准确说是编译器)在重装时是通过参数的静态类型而不是实际类型作为判定依据的。由于静态类型在编译期可知,所以在编译阶段,Javac编译器就根据参数的静态类型决定了会使用哪个重载版本,因此选择了sayHello(Human)作为调用目标,并把这个方法的符号引用写到main()方法里的两条invokevirtual指令的参数中。

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派的最典型应用表现就是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的,这点也是为何一些资料选择把它归入“解析”而不是分派的原因。

需注意Javac编译器虽能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的,往往 只能确定一个“相对更适合”版本。这种模糊的结论在由0和1构成的计算机世界中算比较稀罕,产生这种模糊结论的主要原因是字面量天生的模糊性,它不需要定义,所以字面量就没有显式的静态类型,它的静态类型只能通过语言、语法的规则去理解和推断。

“更合适的”版本(重载方法匹配优先级)代码:

package com.test;

import java.io.Serializable;

public class Overload {

    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 Object");
    }
    
    public static void main(String[] args) {
        sayHello('a');
    }
}

输出结果:

hello char

‘a’是一个char类型的数据,自然会寻找参数类型为char的重载方法,如果注释掉sayHello(char arg)方法,那输出会变为:

hello int

这时发生一次自动类型转换,‘a’除可代表一个字符,还可代表数字97(字符‘a’的Unicode数值为十进制数字97),因此参数类型为int的重载也是合适的,继续注释sayHello(int arg)方法,那输出会变为:

hello long

这时发生两次自动类型转化,'a'转化为整数97后,进一步转化为长整型97L。实际上自动转型还能继续发生多次,按照char>int>long>float>double的顺序转型进行匹配,但不会匹配到byte和short类型的重载,因为char到byte或short的转型是不安全的。我们继续注释掉sayHello(long arg),那输出会变为:

hello Character

这是发生了一次自动装箱,‘a’被包装为它的封装类型java.lang.Character,继续注释掉sayHello(Characterarg),那输出会变为:

hello Serializable

因为java.lang.Serializable是java.lang.Character类实现的一个接口,当自动装箱找不到装箱类,但是找到了装箱类所实现的接口类型,所以紧接着又发生一次自动转型。如果同时出现两个参数分别为Serializable和Comparable<Character>的重载方法,那它们在此时的优先级是一样的。编译器无法确定要自动转型为哪种类型,会提示“类型模糊”(Type Ambiguous),并拒绝编译。
下面继续注释掉sayHello(Serializable arg)方法,输出会变为:

hello Object

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

hello char...

可见变长参数的重载优先级是最低的。

静态方法会在编译期确定、在类加载期就进行解析,而静态方法显然也是可以拥有重载版本的,选择重载版本的过程也是通过静态分派完成的。

相关文章

网友评论

      本文标题:虚拟机字节码执行引擎【方法调用(二)分派之静态分派】

      本文链接:https://www.haomeiwen.com/subject/juotbltx.html