(三十)分派调用:静态分派和动态分派

分派调用

其实分派分为两种,即动态分派和静态分派。我们在了解分派的时候,通常把它们与重写和重载结合到一起。

重载(overload)与静态分派

我们先看一个题:

public class Main {
    static abstract class Father {

    }

    static class Son extends Father {

    }

    static class Daughter extends Father {

    }

    public void getSex(Daughter daughter) {
        System.out.println("i am a girl");
    }

    public void getSex(Son son) {
        System.out.println("i am a boy");
    }

    public void getSex(Father son) {
        System.out.println("i am a father");

    }
    public static void main(String[] args) {

        Father son = new Son();
        Father daughter = new Daughter();
        Main main = new Main();

        main.getSex(son);
        main.getSex(daughter);
    }

}

其实这个栗子就体现了重载。

要是我们在代码里改一下:

main.getSex((Son)son);

就会输出i am a boy

其实这里也体现出了java的静态分派,我们都可以看到main对象已经确认了,那么main在运行main.getSex(son);时选择方法的时候,到底是选择getSex(Son son)还是getSex(Father son)呢?

我们在代码中son的引用类型是Father,但是它的实际类型却是Son

我们再来看看生成的字节码:

字节码里面0-23我们直接跳过,因为0-23对用的代码是

  Father son = new Son();
        Father daughter = new Daughter();
        Main main = new Main();

这里的字节码的作用是创建内存空间,然后把son 、daughter 和main 实例放到第1、2、3个实例变量表Slot中,这里其实还有第0个实例,是this指针,放到的第0个slot中,这个超出了本文要讲解的内容,故跳过。

我们从24看起,aload_x是把刚刚创建的实例放到操作数栈中,然后才能对其操作。后面第26行可以看到:

invokevirtual #50 //Method getSex:(LMain$Father;)

这里相信大家都可以看出,字节码中已经确定了方法的接收者是main和方法的版本是getSex(Father father),所以我们在运行代码的时候,会输出i am a father

其实java编译器在重载的时候是通过参数的静态类型而不是实际类型来确定使用哪个重载的版本的。所以这里在字节码中,选择了getSex(Father father)作为调用目标并把这个方法的符号引用写到main方法的几个invokevirtual指令的参数里面。

所以依赖静态类型来定位方法执行的版本的分派动作成为静态分派。静态分派的典型应用是方法重载,而且静态分派发生在编译期间,因此,静态分派的动作是由编译器发出的。

另外,编译器能确定出方法的重载版本,但在很多的时候,这个版本并不一定是唯一的,比如我把上面的代码改一下:

public class Main {
    static abstract class Father {

    }

    static class Son extends Father {

    }

    public void getSex(Son son) {
        System.out.println("i am a boy");
    }

    public void getSex(Father son) {
        System.out.println("i am a father");

    }
    public static void main(String[] args) {

        Son son = new Son();
        Main main = new Main();

        main.getSex(son);
    }

}

然后再输出:

QQ截图20160724221829.png

这是很正常的执行结果,要是我们把getSex(Son son)注释掉,然后再运行试试:

发现,编译器并找不到getSex(Son son)这个方法,只有作出适当的妥协,把son向上转型为Father,然后选择了getSex(Father son)方法。

要是我们再把getSex(Father son)注释掉,会发现:

这里又选择了妥协并向上继续转型成Object。

综上所述:静态分派是选择的最合适的一个方法版本来重载,然而这个版本并不是唯一确定的。我们在写代码的时候,要尽量避免这种情况发生,虽然这似乎能显示出你知识的很渊博,但这并不是一个明智的选择。

重写(override)与动态分派

看完了静态分派,我们再来看看动态分派。动态分派经常与重写紧密联系在一起,那么我们就先来看一个重写的栗子:

public class Main {
    static  class Father {
        public void say(){
            System.out.println("i am fasther");
        }
    }

    static class Son extends Father {

        @Override
        public void say() {
            System.out.println("i am son");
        }

    }

    static class Daughter  extends Father {

        @Override
        public void say() {
            System.out.println("i am daughter ");
        }

    }
    public static void main(String[] args) {

        Father son = new Son();
        Father daughter = new Daughter();

        son.say();
        daughter.say();

    }

}

output:

i am son
i am daughter

相信大家都知道输出结果是什么,三个类都有say()方法,但是虚拟机是怎样知道调用哪个方法的呢? 别急,我们还是按照惯例,看看字节码:

现在相信大家大概都能看懂里面字节码是怎样回事了吧?

我们发现第17行和第21行对应的java代码应该是:

     son.say();
        daughter.say()

从字节码来看,这两行代码是一样的。调用了同一个类的同一个方法,都是Father.say(),那为什么他们最后的输出却不一样??

这里的原因其实要从invokevirtual的多态查找开始说起,invokevirtual指令运行时的解析过程大概如下:

  • 找到操作数栈的栈顶元素所指向的对象的实际类型,记作C
  • 如果在类型C中找到与描述符和简单名称都相符的方法,则进行访问权限校验。通过则放回这个方法的直接引用,否则返回illegalaccesserror
  • 否则,则按照继承关系从下住上依次对C的父类进行步骤2的查找。
  • 如果始终没有找到合适的方法,则跑出AbstractMethodError异常。

由于invokevirtual指令在执行的第一步就对运行的时候的接收者的实际类型进行查找,所以上面两次调用的invokevirtual指令都能成功找到实际类型的say()方法,然后把类方法的符号引用解析到不同的直接引用上面,这也是重写的体现。

然后这种运行期根据实际类型来判断方法的执行版本的分派过程叫作动态分派。

原文地址:https://www.cnblogs.com/shyroke/p/9175530.html

时间: 2024-08-29 12:16:04

(三十)分派调用:静态分派和动态分派的相关文章

队列的三种实现(静态数组、动态数组及指针)

本文有关栈的介绍部分参考自网站数据结构. 1. 队列  1.1 队列的定义 队列(Queue)是只允许在一端进行插入,而在另一端进行删除的运算受限的线性表. (1)允许删除的一端称为队头(Front). (2)允许插入的一端称为队尾(Rear). (3)当队列中没有元素时称为空队列. (4)队列亦称作先进先出(First In First Out)的线性表,简称为FIFO表.    队列的修改是依先进先出的原则进行的.新来的成员总是加入队尾(即不允许"加塞"),每次离开的成员总是队列头

栈的三种实现(静态数组、动态数组及指针)

本文有关栈的介绍部分参考自网站数据结构. 1. 栈  1.1 栈的定义 栈(Stack)是限制仅在表的一端进行插入和删除运算的线性表. (1)通常称插入.删除的这一端为栈顶(Top),另一端称为栈底(Bottom). (2)当表中没有元素时称为空栈. (3)栈为后进先出(Last In First Out)的线性表,简称为LIFO表.    栈的修改是按后进先出的原则进行.每次删除(退栈)的总是当前栈中"最新"的元素,即最后插入(进栈)的元素,而最先插入的是被放在栈的底部,要到最后才能

多态性实现机制——静态分派与动态分派

方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给 Java 带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析.这在前面的“Java 内存区域与内存溢出”一文中有提到. 静态解析成立的前提是:方法在程序真正执行前就有一个可确定的调用版本,并

Java方法重载与重写(静态分派与动态分派)

Java面向对象3个基本特征:继承.封装和多态:多态主要体现在重载和重写: 1.静态分派 静态分派与重载有关,虚拟机在重载时是通过参数的静态类型,而不是运行时的实际类型作为判定依据的:静态类型在编译期是可知的: 1)基本类型 以char为例,按照char>int>long>double>float>double>Character>Serializable>Object>...(变长参数,将其视为一个数组元素) 变长参数的重载优先级最低 (注意char

Java静态分派与动态分派(二)

方法调用并不等于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程. 在程序运行时,进行方法调用是最普遍.最频繁的操作,但是Class文件的编译过程不包括传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相对于之前说的直接引用).这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方

JVM 方法调用之动态分派

1. 动态分派 一个体现是重写(override).下面的代码,运行结果很明显.  1 public class App { 2      3     public static void main(String[] args) { 4         Super object = new Sub(); 5         object.f(); 6     } 7 } 8  9  class Super {10     public void f() {11         System.ou

浅谈动态分派和静态分派

前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new Son(); //静态分派 print(father); } void print(Father father) { System.out.println("this is father"); } void print(Son son) { System.out.println("

Java静态分派和动态分派

前言 动态分派和静态分派机制是Java多态实现的原理.本文将针对这两种机制进行浅析. 静态分派 静态分派机制最典型的代码示例如下 void test() { Father father = new Son(); //静态分派 print(father); } void print(Father father) { System.out.println("this is father"); } void print(Son son) { System.out.println("

Jvm(65),方法调用----静态分派调用

我们先来看一下下面的例子: package demo.jvm.test8? public class Demo2 { /** 方法静态分派演示 * @author zzm */ static abstract class Human { } static class Man extends Human { } static class Woman extends Human { } public void sayHello(Human guy) { System.out.println("hel