Java 中多态的实现原理

一. 什么是多态

多态是面向对象编程里面的概念,一个接口的多种不同的实现方式,即为多态。

这里的接口不应理解得太死板,比如在 Java 里面,继承一个类和实现一个接口本质上都是一种继承行为,因此都可以理解为多态的体现。

从静态和动态的角度进行划分,多态可以分为 编译时多态 和 运行时多态

编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法,编译之后会变成两个不同的方法。

而运行时多态是动态的,是通过动态绑定来实现的,也就是大家通常所说的多态性,本篇博客主要讨论运行时多态。

二. 多态的特点

多态的特点为:只有在运行的时候才知道引用指向的是哪个类的实例对象,以及引用调用的方法指向的是哪个类中实现的方法。

多态通常有两种实现方法:

  • 子类继承父类(extends)

  • 类实现接口(implements)

多态核心之处就在于对父类方法的重写或对接口方法的实现,以此在运行时实现不同的执行效果。

三. 多态的代码实现

1. 子类继承父类

第一步:定义父类

public class Father {
    public void dealHouse(){
        System.out.println("父亲处置房产"); 
    }    
}

第二步:定义子类(大儿子和小儿子)

//大儿子
public class SonA extends Father {
    @Override
    public void dealHouse() {
        System.out.println("大儿子处置房产"); 
    }
}
//小儿子
public class SonB extends Father {
    @Override
    public void dealHouse() {
        System.out.println("小儿子处置房产"); 
    }
}

第三步:测试

public class Test {
    public static void main(String[] args) {
        Father father = new Father(); 
        Father sonA = new SonA();
        Father sonB = new SonB();     
        father.dealHouse();
        sonA.dealHouse();
        sonB.dealHouse();
    }
}

2. 类实现接口

第一步:定义父类接口

public interface Father {
    public void dealHouse();
}

 第二步:实现父类接口

//大儿子
public class SonA implements Father {

    public void dealHouse() {
        System.out.println("大儿子处置房产"); 
    }
}
//小儿子
public class SonB implements Father {

    public void dealHouse() {
        System.out.println("小儿子处置房产"); 
    }
}

这里我们从代码的角度对多态进行了演示,接下来我们具体来分析下多态的实现原理。 

四. 多态的实现原理

RTTI

多态实现的技术基础是 RTTI,即 Run-Time Type Identification(运行时类型判定),它的作用是在我们不知道某个对象的确切的类型信息时(即某个对象是哪个类的实例),可以通过 RTTI 相关的机制帮助我们在编译时获取对象的类型信息。

而 RTTI 的功能主要是通过 Class 类文件实现的,更精确一点来说,是通过 Class 类文件的方法表实现的。

这里提到的 Class 类可以理解为是 "类的类"(class of classes)。如果说类是对象的抽象的话,那么 Class 类就是对类的抽象。而每个类都有一个 Class 对象,每当编写并且编译成功一个新的类,就会生成一个对应的 Class 对象,被保存在一个与类同名的 .class 文件中。

详细一点来说,就是 Java 源码编译器将 Java 文件编译成 .class 文件,然后通过类装载器将 .class 文件装载到 JVM 中,并在内部建立该类的类型信息(.class 文件在 JVM 中存储的一种数据结构),最后通过执行引擎来执行。

从图片中可以看出,JVM 分了五个区域,那么在代码中定义的那些多态方法存到了哪个地方呢?

刚刚我们提到,编译过程会在 JVM 内部建立对应类的类型信息,类型信息包括了方法代码,类变量、成员变量、以及方法表。根据我们对 JVM 各个区域的功能了解,就可以知道类型信息是存储在方法区的,也就是说,方法区是多态方法实现的关键。

方法区的类型信息中会增加一个指针,用来优化对象调用方法的速度,该指针指向一张记录该类方法入口的表,这张表就是我们接下来要重点讨论的方法表。

方法表

方法表是实现多态的关键所在,里面保存的是实例方法的引用,且是直接引用。Java 虚拟机在执行程序时,就是通过方法表来确定运行哪一个多态方法的。

方法表的构造如下:


可以看出,最先存放的是 Object 类的方法,接下来是该类的父类的方法,最后是该类本身的方法。这里有一个需要特别注意的地方:

如果子类改写了父类的方法,那么子类和父类的同名方法共享一个方法表项,都被认作是父类的方法,如下所示:

由于子类和父类的同名方法共享一个方法表项,所以方法表的偏移量总是固定的。所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。

了解了方法表的构造,接下来我们就可以结合它的特点来分析多态方法的调用过程了:

多态方法调用

在调用方法时,首先需要完成实例方法的符号引用解析,也就是将符号引用解析为方法表的偏移量。

虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法;

此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中根据偏移量获取该方法名对应的指针,进而就能指向实际类的方法了。

上面我们讨论的仅是利用继承实现多态的内部机制,多态的另外一种实现方式:接口实现相比而言会更加复杂。原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。

所以在 JVM 中,多态的实例方法调用实际上有两种指令:

  • invokevirtual 指令:用于调用声明为类的方法;

  • invokeinterface 指令:用于调用声明为接口的方法。

当使用 invokeinterface 指令调用方法时,就不能采用固定偏移量的办法了。实际上,Java 虚拟机对于接口方法的调用是采用搜索方法表的方式来实现的,比如,要在 Father 接口的方法表中找到 dealHouse() 方法,必须搜索 Father 的整个方法表。所以我们可以得出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的。

以上就是多态的原理,总结起来说就是两点:

  1. 方法表起了决定性作用,如果子类改写了父类的方法,那么子类和父类的同名方法共享一个方法表项,都被认作是父类的方法,因此可以写成父类引用指向子类对象的形式。

  2. 类和接口的多态实现不一样,类的方法表可以使用固定偏移,但接口需要进行搜索,原因是接口的实现不是确定唯一的,所以相对来说性能差一些。


本篇博客参考文章如下,特别感谢,献上花花~:

https://mp.weixin.qq.com/s/D94zyd6JSjITDgR2UxeyzQ

https://zhuanlan.zhihu.com/p/75017810

https://www.cnblogs.com/kaleidoscope/p/9790766.html

Guess you like

Origin blog.csdn.net/j1231230/article/details/119935541