学习Java的时候就已经学习了多态但是对于多态的原理还是不是很明白所以写下这篇文章来记录一下 如果后面有新的理解也继续添加到本页面
什么是多态
多态是面向对象编程里面的概念,一个接口的多种实现不同的实现方式,即为多态 这里的接口不应理解得太死板,比如在 Java 里面,继承一个类和实现一个接口本质上都是一种继承行为,因此都可以理解为多态的体现。
从静态和动态的角度进行划分,多态可以分为 编译时多态 和 运行时多态。
编译时多态是静态的,主要是指方法的重载,它是根据参数列表的不同来区分不同的方法,编译之后会变成两个不同的方法。
而运行时多态是动态的,是通过动态绑定来实现的,也就是大家通常所说的多态性,本篇博客主要讨论运行时多态。
多态的特点
多态的特点为:只有在运行的时候才知道引用指向的是哪个类的实例对象,以及引用调用的方法指向的是哪个类中实现的方法。 多态通常有两种实现方法:
- 子类继承父类(extends)
- 类实现接口(implements)
多态核心之处就在于对父类方法的重写或对接口方法的实现,以此在运行时实现不同的执行效果
多态的原理
RTTI,即Run-Time Type Identification运行时类型认定,通过运行时类型信息程序能够使用父类的指针或引用来检查这些指针或引用所指的对象的实际派生类型,是多态实现的技术基础。RTTI的功能主要是通过Class类文件实现的,更精确一点是通过Class类文件的方法表实现的。
Class类是"类的类"(class of classes)。如果说类是对象的抽象的话,那么Class类就是对类的抽象。Class对象就是用来创建一个类的所有的常规对象的。每个类都有一个Class对象,每当编写好并且编译了一个新的类,就会生成一个它对应的Class对象,被保存在一个与类同名的.class文件中。java虚拟机中的被称为类加载器的子系统,就是专门拿来做生成这个类的Class对象的工作的。
代码语言:javascript复制public class TT {
public static void main(String[] args) {
P p1 = new Man("男孩");
System.out.println(p1.getClass().getName());
P p2 = new WoMan("女孩");
System.out.println(p2.getClass().getName());
}
}
class P{
String name;
public P(String name){
this.name = name;
}
public void say(){
System.out.println(name "is runing");
}
}
class Man extends P{
String name;
public Man(String name) {
super(name);
this.name = name;
}
}
class WoMan extends P{
String name;
public WoMan(String name) {
super(name);
this.name = name;
}
}
可以发现即使我们将对象的引用向上转型,对象所指向的Class类对象依然是实际的实现类。
Java中每个对象都有相应的Class类对象,因此,我们随时能通过Class对象知道某个对象“真正”所属的类。无论我们对引用进行怎样的类型转换,对象本身所对应的Class对象都是同一个。这意味着java在运行时的确能确定真正的实现类是哪一个。
可以发现,就算是这种起始这个也是保持的是自己的字节码对象而不是父类的字节码
假如子类在重写了父类的方法那么会在链接的解析阶段将父类中方法的引用指向子类中重写的方法
多态方法调用
在调用方法时,首先需要完成实例方法的符号引用解析,也就是将符号引用解析为方法表的偏移量。
虚拟机通过对象引用得到方法区中类型信息的入口,查询类的方法表,当将子类对象声明为父类类型时,形式上调用的是父类方法;
此时虚拟机会从实际类的方法表(虽然声明的是父类,但是实际上这里的类型信息中存放的是子类的信息)中根据偏移量获取该方法名对应的指针,进而就能指向实际类的方法了。
上面我们讨论的仅是利用继承实现多态的内部机制,多态的另外一种实现方式:接口实现相比而言会更加复杂。原因在于,Java的单继承保证了类的线性关系,而接口可以同时实现多个,这样光凭偏移量就很难准确获得方法的指针。
所以在 JVM 中,多态的实例方法调用实际上有两种指令:
invokevirtual 指令:用于调用声明为类的方法;
invokeinterface 指令:用于调用声明为接口的方法。
当使用 invokeinterface 指令调用方法时,就不能采用固定偏移量的办法了。实际上,Java 虚拟机对于接口方法的调用是采用搜索方法表的方式来实现的,比如,要在 Father 接口的方法表中找到 dealHouse() 方法,必须搜索 Father 的整个方法表。所以我们可以得出,在性能上,调用接口引用的方法通常总是比调用类的引用的方法要慢。这也告诉我们,在类和接口之间优先选择接口作为设计并不总是正确的。
以上就是多态的原理,总结起来说就是两点:
- 方法表起了决定性作用,如果子类改写了父类的方法,那么子类和父类的同名方法共享一个方法表项,都被认作是父类的方法,因此可以写成父类引用指向子类对象的形式。
- 类和接口的多态实现不一样,类的方法表可以使用固定偏移,但接口需要进行搜索,原因是接口的实现不是确定唯一的,所以相对来说性能差一些。
因为接口的实现不是唯一的,所以性能会低一点