方法调用与劣质面试题

前言

运行一个程序就是将PC寄存器的值设置为程序入口地址,当有方法跳转时就是将PC置为方法的起始地址。在字节码层面,一切方法调用在Class文件里面存储的都是符号引用,而不是方法在内存中的入口地址,这个特性给Java带来了更强大的动态扩展能力。

这里方法调用的意思是确定调用哪一个方法,不涉及方法内部的具体运行过程。为什么这是个问题呢?面向对象编程语言存在重载,重写,虚方法的概念,有些情况下只有在运行时才能确定要执行的方法。

这里称之为“劣质面试题”,是因为正常情况下写代码不会出现这种写法,并且无论对Java理解多深刻在工作中都应当极力避免出现这种写法,这些题目是特意编出来考察对Java虚拟机方法调用的理解。

通用实现

过程即编程语言中的函数,方法,子例程,处理函数等,叫法不同但都是一个意思。假设过程P调用过程Q,Q执行完后返回到P,要实现这些操作需要:

  • 传递控制:在进入Q时,要将PC设置为Q代码的起始地址,Q返回时要将PC设置为P方法中调用Q后面的那条指令地址。
  • 传递参数:P向Q提供参数,Q向P提供返回值。
  • 分配和释放内存:Q可能需要分配局部变量内存,执行完返回后要释放这些内存。

实现过程调用机制的关键在于使用数据结构提供的后进先出的内存管理原则。

解析调用

如果调用一个方法没有任何歧义,比如调用类的私有方法,就可以在编译时确定要执行的方法。在类加载的解析阶段,会将这部分符号引用转化为直接引用,这类方法的调用被称为解析。Java语言里的静态方法、私有方法、实例构造器、父类方法以及上被final 修饰的方法,这5种方法适用解析调用,这些方法统称为“非虚方法”,解析调用一定是个静态的过程,在编译期间就完全确定要执行的方法。其他方法被称为“虚方法”。

静态分派

重载是方法名相同但参数类型或者个数不同,通常情况下参数数据类型有着很大的差异,比如:

public void a(String s) {}
public void a(int i) {}
public void a(String s,int i){}
//调用 a("ss")不会有任何歧义

但是,如果是这样的呢:

//劣质面试题
class Human{}
class Man{}
class Woman{}
    
public void a(Human h) {}
public void a(Man m) {}

//调用
Human h=new Man();
a(h);
//实际上会执行a(Human h)

Human man = new Man();对于这行代码,静态类型为Human,实际类型为Man,静态类型是在编译期可知的,实际类型变化的结果在运行期才可确定。

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

虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据的。所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派

动态分派

    class Human {
        public void sayHello() {
            System.out.println("human say hello");
        }
    }
    class Man extends Human {
        @Override
        public void sayHello() {
            System.out.println("man say hello");
        }
    }
    public static void main(String[] args) {
        Human human = new Man();
        human.sayHello();//输出man say hello
    }

上述代码的结果不会有歧义,但是根本原因是什么?Java中实现重写的本质是根据方法接收者的实际类型来选择方法版本,具体实现是invokevirtual指令。

invokevirtual指令用于调用所有的虚方法,该指令的运行过程第一步是找到操作数栈顶的第一个元素所指向的对象的实际类型。根据上面虚方法/非虚方法的定义,能重写的方法必然是个虚方法。

字段没有多态性

invokevirtual指令只针对方法,事实上Java里面只有虚方法存在, 字段不可能是虚的,字段永远不参与多态。当子类声明了与父类同名的字段时,虽然在子类的内存中两个字段都会存在,但是子类的字段会遮蔽父类的同名字段。

//劣质面试题
    static class Father {
        private int money;
        public Father() {
            money = 2;
            //虚方法调用取决于实际类型
            show();
        }
        public void show() {
            System.out.println("Father:" + this.money);
        }
    }

    static class Son extends Father {
        private int money;
        public Son() {
            money = 4;
            show();
        }
        @Override
        public void show() {
            //字段不参与多态,打印的永远都是本类中的money
            System.out.println("Son:" + this.money);
        }
    }
    public static void main(String[] args) {
        Father father = new Son();
        System.out.println("father:" + father.money);
        //输出:
        //Son:0
        //Son:4
        //father:2
    }

Son类在创建的时候,首先隐式调用了Father的构造方法,而 Father构造方法中对show()的调用是一次虚方法调用,这里的实际类型是Son,所以实际执行的版本是 Son的show(),又因为此时Son还没有执行构造方法,Son中money的值为0,所以第一句输出Son:0。Son的构造方法执行完后,继续执行Son的show(),输出4,最后输出Father类的中的money的值为2。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章