一个对象变量能够指示多种实际类型的现象称为多态html
容许不一样类的对象对同一消息作出响应。方法的重载、类的覆盖正体现了多态。java
一、编译时多态(又称静态多态) 二、运行时多态(又称动态多态)
重载(overload 发生在一个类中,方法名必须相同,不一样参数)就是编译时多态的一个例子,编译时多态在编译时就已经肯定,运行时运行的时候调用的是肯定的方法。app
咱们一般所说的多态指的都是运行时多态,也就是编译时不肯定究竟调用哪一个具体方法,一直延迟到运行时才能肯定。这也是为何有时候多态方法又被称为延迟方法的缘由。this
下面简要介绍一下运行时多态(如下简称多态)的机制。es5
一、子类继承父类(extends) 二、类实现接口(implements)
不管是哪一种方法,其核心之处就在于对父类方法的改写或对接口方法的实现,以取得在运行时不一样的执行效果。spa
要使用多态,在声明对象时就应该遵循一条法则:声明的老是父类类型或接口类型,建立的是实际类型。.net
Java 的方法调用有两类,动态方法调用与静态方法调用。静态方法调用是指对于类的静态方法的调用方式,是静态绑定的;而动态方法调用须要有方法调用所做用的对象,是动态绑定的。类调用 (invokestatic) 是在编译时刻就已经肯定好具体调用方法的状况,而实例调用 (invokevirtual) 则是在调用的时候才肯定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。指针
JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。本文也能够说是对于 JVM 后两种调用实现的考察。code
常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。htm
常量池在逻辑上能够分红多个表,每一个表包含一类的常量信息,本文只探讨对于 Java 调用相关的常量池表。
CONSTANT_Utf8_info
字符串常量表,该表包含该类所使用的全部字符串常量,好比代码中的字符串引用、引用的类名、方法的名字、其余引用的类与方法的字符串描述等等。其他常量池表中所涉及到的任何常量字符串都被索引至该表。
CONSTANT_Class_info
类信息表,包含任何被引用的类或接口的符号引用,每个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。
CONSTANT_NameAndType_info
名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。
CONSTANT_InterfaceMethodref_info
接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。
CONSTANT_Methodref_info
类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引。
能够看到,给定任意一个方法的索引,在常量池中找到对应的条目后,能够获得该方法的类索引(class_index)和名字类型索引 (name_and_type_index), 进而获得该方法所属的类型信息和名称及描述符信息(参数,返回值等)。注意到全部的常量字符串都是存储在 CONSTANT_Utf8_info 中供其余表索引的。
方法表是动态调用的核心,也是 Java 实现动态调用的主要方式。它被存储于方法区中的类型信息,包含有该类型所定义的全部方法及指向这些方法代码的指针,注意这些具体的方法代码多是被覆写的方法,也多是继承自基类的方法。
若有类定义 Person, Girl, Boy,
class Person { public String toString(){ return "I'm a person."; } public void eat(){} public void speak(){} } class Boy extends Person{ public String toString(){ return "I'm a boy"; } public void speak(){} public void fight(){} } class Girl extends Person{ public String toString(){ return "I'm a girl"; } public void speak(){} public void sing(){} }
当这三个类被载入到 Java 虚拟机以后,方法区中就包含了各自的类的信息。Girl 和 Boy 在方法区中的方法表可表示以下:
能够看到,Girl 和 Boy 的方法表包含继承自 Object 的方法,继承自直接父类 Person 的方法及各自新定义的方法。注意方法表条目指向的具体的方法地址,如 Girl 的继承自 Object 的方法中,只有 toString() 指向本身的实现(Girl 的方法代码),其他皆指向 Object 的方法代码;其继承自于 Person 的方法 eat() 和 speak() 分别指向 Person 的方法实现和自己的实现。
Person 或 Object 的任意一个方法,在它们的方法表和其子类 Girl 和 Boy 的方法表中的位置 (index) 是同样的。这样 JVM 在调用实例方法其实只须要指定调用方法表中的第几个方法便可。
如调用以下:
class Party{ … void happyHour(){ Person girl = new Girl(); girl.speak(); … } }
当编译 Party 类的时候,生成 girl.speak()
的方法调用假设为:
Invokevirtual #12
设该调用代码对应着 girl.speak(); #12 是 Party 类的常量池的索引。JVM 执行该调用指令的过程以下所示:
JVM 首先查看 Party 的常量池索引为 12 的条目(应为 CONSTANT_Methodref_info 类型,可视为方法调用的符号引用),进一步查看常量池(CONSTANT_Class_info,CONSTANT_NameAndType_info ,CONSTANT_Utf8_info)可得出要调用的方法是 Person 的 speak 方法(注意引用 girl 是其基类 Person 类型),查看 Person 的方法表,得出 speak 方法在该方法表中的偏移量 15(offset),这就是该方法调用的直接引用。
当解析出方法调用的直接引用后(方法表偏移量 15),JVM 执行真正的方法调用:根据实例方法调用的参数 this 获得具体的对象(即 girl 所指向的位于堆中的对象),
具体过程:
假设类B是类A的子类,以 A a = new B() 为例
① A a 做为一个引用类型数据,存储在JVM栈的本地变量表中。
② new B()做为实例对象数据存储在堆中
B的对象实例数据(接口、方法、field、对象类型等)的地址也存储在堆中
B的对象的类型数据(对象实例数据的地址所执行的数据)存储在方法区中,方法区对象类型数据中有一个指向该类方法的方法表。
③Java虚拟机规范中并未对引用类型访问具体对象的方式作规定,目前主流的实现方式主要有两种:
1. 经过句柄访问
在这种方式中,JVM堆中会专门有一块区域用来做为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法因为用句柄表示地址,所以十分稳定。
2.经过直接指针访问
经过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优点是速度快,在HotSpot虚拟机中用的就是这种方式。
④实现过程
首先虚拟机经过reference类型(A的引用)查询java栈中的本地变量表,获得堆中的对象类型数据的地址,从而找到方法区中的对象类型数据(B的对象类型数据) ,而后查询方法表定位到实际类(B类)的方法运行。
据此获得该对象对应的方法表 (Girl 的方法表 ),进而调用方法表中的某个偏移量所指向的方法(Girl 的 speak() 方法的实现)。
参考:http://www.cnblogs.com/loveincode/p/7230448.html;
https://blog.csdn.net/huangrunqing/article/details/51996424;
http://www.codes51.com/article/detail_701880.html;