• 深入理解Java多态机制


    从字节码层面来看,Java中的所有方法调用,最终无外乎转换为如下几条调用指令。

    • invokestatic: 调用静态方法。
    • invokespecial: 调用实例构造器<init>方法,私有方法和父类方法。
    • invokevirtual: 调用所有的虚方法。
    • invokeinterface: 调用接口方法,会在运行时再确定一个实现此接口的对象。
    • invokedynamic: 调用动态方法。JDK 7引入的,主要是为了支持动态语言的方法调用。

    JVM提供了上述5条方法调用指令,所以不妨从字节码层面来一窥Java多态机制的执行过程。

    1 虚方法和非虚方法

    上述5条方法调用指令中的invokevirtual负责调用所有的虚方法。那么什么是虚方法?什么是非虚方法呢?

    从Java语言层面来看,static,private,final修饰的方法,父类方法以及实例构造器,这些方法称为非虚方法。与之相反,其他所有的方法称为虚方法。

    字节码指令层面来讲,invokestatic和invokespecial调用的方法都是非虚方法。

    2 静态类型和实际类型

    先看一看以下代码的定义:

    Human man = new Man();

    我们把 Human称为变量的静态类型,Man称为变量的实际类型。

    引用变量都有两个类型:静态类型(定义该变量的类型)和实际类型(实际指向的对象的类型)。

    静态类型是编译时可知的,而实际类型只有运行时才能确定。

    3 Java中多态机制的实现过程

    invokevirtual指令的运行时解析过程大致分为如下几个步骤:

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

    正是由于invokevirtual指令是这样的一个执行过程,所以这就解释了为什么java语言里面实现多态需要如下三个条件:a. 父类引用指向子类对象。b. 有继承的存在。c. 子类重写父类方法。

    • 由于父类引用指向子类对象,所以jvm会去首先去查找该子类对象对应的类型。
    • 又由于有继承的存在,所以子类的方法不可能比父类少,这就保证了,只要该引用变量能调用的方法,子类中一定存在。所以第二步一定能在子类的类型中查找到调用的方法。
    • 方法找到后就可以执行了,至于方法执行后能不能产生不同的效果(多态),得看子类是否重写了这个方法。所以要想产生多态,子类得重写父类方法。【注意:以上所说的方法均是指的虚方法。】

    4 静态分派和动态分派

    分派过程会揭示多态的一些最基本的体现,比如”重写“和“重载”在Java虚拟机中是如何实现的。

    4.1 静态分派

    所有依赖(实参的)静态类型来定位方法执行版本的分派动作称为静态分派。典型的应用就是方法重载(Overload)。

    先看一个例子:

    public class StaticDispatch {
        static class Human {
        }
    
        static class Man extends Human {
        }
    
        static class Women extends Human {
        }
    
        public void sayHello(Human guy) {
            System.out.println("hello, guy!");
        }
    
        public void sayHello(Man guy) {
            System.out.println("hello, man!");
        }
    
        public void sayHello(Women guy) {
            System.out.println("hello, women!");
        }
    
        public static void main(String[] args) {
            Human man = new Man();
            Human women = new Women();
            StaticDispatch sd = new StaticDispatch();
            sd.sayHello(man);
            sd.sayHello(women);
        }
    
    }

    输出结果:

    hello, guy!
    hello, guy!

    没错,程序就是大家熟悉的重载(Overload)。在上述程序中,由于方法的接受者已经确定是StaticDispatch的实例sd了,所以最终调用的是哪个重载版本也就取决于传入参数的类型了。

    Java中重载的本质

    编译器在重载时是根据传入实参的静态类型而不是实际类型作为判定依据的。静态类型是编译时可知的,所以在编译阶段,编译器会根据实参的静态类型决定调用那个重载版本。

    4.2 动态分派

    在运行期根据变量的实际类型来确定方法执行版本的分派过错称为动态分派。典型的应用就是重写(override)。例子如下:

    public class DynamicDispatch {
     
        public static void main(String[] args) {
             
            Human man = new Man();
            Human women = new Women();
             
            man.sayHello();
            women.sayHello();
             
            man = new Women();
            man.sayHello();
     
        }
     
    }
     
    abstract class Human {
        protected abstract void sayHello();
    }
     
    class Man extends Human {
     
        @Override
        protected void sayHello() {
            System.out.println("hello man!");
        }
         
    }
     
    class Women extends Human {
     
        @Override
        protected void sayHello() {
            System.out.println("hello women!");
        }
         
    }

    输出结果:

    hello man!
    hello women!
    hello women!

    Java中重写的本质

    invokevirtual指令的运行时的解析过程。

    4.3 综合例子

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

    输出结果:

    Father choose 360
    Son choose qq

    分析如下:

         Father father = new Father();
            Father son = new Son();
            
            /**
             * Father里面有两个重载的hardChoice方法。所以会根据hardChoice()的实参的【静态类型】来决定调用哪个版本的方法。
             */
            father.hardChoice(new _360());    
            
            /**
             * 变量son的静态类型是Father,实际类型是Son。并且类Son重写了父类Father里面的两个重载的hardChoice方法。
             * 所以运行的时候首先会确定调用子类Son里面的方法,然后在根据hardChoice()的实参的【静态类型】来决定调用Son里面的哪个版本的方法。
             */
            son.hardChoice(new QQ());

    5 虚拟机动态分派的实现

    • 由于动态分派是非常频繁的操作,基于性能考虑,在JVM的具体实现中常常 做一些优化。最常用的“稳定优化”手段就是为类在方法区中建立一个虚方法表(Virtual Method Table,也称vtable)。与此对应的,invokeinterface执行时也会用到接口方法表(Interface Method Table,简称itable)。
    • 虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那么子类的虚方法表里的地址入口和父类相同方法的地址入口是一致的。如果子类重写了这个方法,子类方法表中的地址就会被替换为指向子类实现版本的入口地址。


    参考资料

    http://blog.csdn.net/kobejayandy/article/details/39620679

    http://www.cnblogs.com/jack204/archive/2012/10/29/2745150.html

    http://blog.csdn.net/oypc2303/article/details/4393831

    http://blog.csdn.net/lidaweihgy/article/details/7660346

    http://www.cnblogs.com/qinqinmeiren/archive/2011/07/15/2151687.html

    http://www.geekcome.com/content-10-4128-1.html


  • 相关阅读:
    QT Launching Debugger 卡住
    VS无法打开源文件"stdio.h"
    QT 5.15 https 无法访问 TLS initialization failed
    .Net 添加第三方控件
    XML解析——DOM解析
    javascript——对象
    SQL Server 合并表 union 和union all
    深入理解HTTP
    HTTP详解(3)-http1.0 和http1.1 区别
    HTTP详解(2)
  • 原文地址:https://www.cnblogs.com/tsiangleo/p/4415628.html
Copyright © 2020-2023  润新知