JAVA的多态性
多态性就是同一个行为的不同的表现形式。
多态性的必要条件
- 子类继承父类
- 具体性对抽象有覆写
- 父类的引用指向之类对象
多态性的实现途径
- 重写,重写指的是子类对父类方法的重新改写,注意值方法体中的逻辑改写,方法的签名和返回类型是一致的。
- 重载,指的是方法有相同的名字,但是参数列表不一致的时候的一种调用方式,此时会根据方法的签名进行匹配,得到正确的方法后执行。
- 接口实现,接口是一类无法被实例化,但是可以被实现的抽象类,它里面只有抽象方法,只是不用声明为抽象方法。
多态性的具体实现
类的装载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
此结构中,我们只探讨和本文密切相关的方法区 (method area)。当程序运行需要某个类的定义时,载入子系统 (class loader subsystem) 装入所需的 class 文件,并在内部建立该类的类型信息,这个类型信息就存贮在方法区。类型信息一般包括该类的方法代码、类变量、成员变量的定义等等。可以说,类型信息就是类的 Java 文件在运行时的内部结构,包含了改类的所有在 Java 文件中定义的信息。
注意到,该类型信息和 class 对象是不同的。class 对象是 JVM 在载入某个类后于堆 (heap) 中创建的代表该类的对象,可以通过该 class 对象访问到该类型信息。比如最典型的应用,在 Java 反射中应用 class 对象访问到该类支持的所有方法,定义的成员变量等等。可以想象,JVM 在类型信息和 class 对象中维护着它们彼此的引用以便互相访问。两者的关系可以类比于进程对象与真正的进程之间的关系。简单的说就是直接给我们加载到JVM 中的类型信息的话,我们很难进行操作,而且类型信息数据机构也比较复杂,因此,为我们提供了一个class对象,这样我们就可以通过这个封装好的对象调用到类型信息结构体了。类型信息的结构体中,的方法信息,只包含本类特有的,或者是重写的方法信息,没有父类的方法信息。
方法的调用
Java 的方法调用有两类,动态方法调用与静态方法调用。静态方法调用是指对于类的静态方法的调用方式,是静态绑定的;而动态方法调用需要有方法调用所作用的对象,提供一个this指针,这样可以访问到自己的对象,从而得到自己对应的方法表(父类引用指向子类,也就是this指向子类,这样就可以得到子类的方法表)。类调用 (invokestatic) 是在编译时刻就已经确定好具体调用方法的情况,而实例调用 (invokevirtual) 则是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。
方法表
方法表示基于上面的类型信息结构体抽象出来的,它指向类型信息结构体,但是它的作用就是根据方法的符号名称或者偏移量提供具体方法地址指针的操作,类似于索引。每个类都会有一个方法表,子类的如果覆写了父类的方法,那么该方法对应的指针就直接指向子类自己的类型信息结构体中,否则就指向父类的类型信息结构体中。方法表的索引主要有两种方式:类继承的时候根据偏移量,接口的时候根据搜索。
实现流程
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(){}
}
public static main(String args[]){
Person girl = new Girl();
girl.speak();
}
- 查看Person的方法表,根据符号引用得到直接引用,也就是偏移量。
- 根据this指针的真正指向,访问该真正的对象,进而得到该对象的类型信息中的方法表
- 根据偏移量得到this所指向的具体实现类的方法的地址。
- 指向具体的实例的方法调用,实现动态调用。
注意:这里其实有两次的查表,首先是我们需要查自己的Person类的方法表,因为我本来就是这个范围类的,因此我只能访问自己这个范围内的方法,例如子类的自定义的肯定是访问不了的!!通过常量池中的表格进行符号搜索,得到该方法在本方法表中的索引-偏移量或者直直接引用,之后根据得到的****索引,去this指针指向的对象的方法表中调用这个方法即可。
接口的方法调用
以上的流程是针对的继承类的时候,可以直接使用偏移量来做,但是如果是接口的话,那么每个类的接口中的方法的偏移量就不一样了,因此,这里在常量池中搜索得到方法的符号引用,这时候不查询本地的方法表,而是直接去查询this 指向的实例的方法表,而且是通过搜索的方式来得到方法的地址。相对子类的每一步都有索引的机制,这种机制速度会比较慢。因此,接口调用的时候,其实只有一次直接查表的操作,而且查表是遍历搜索的。
再说一下常量池(constant pool)
常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。
常量池在逻辑上可以分成多个表,每个表包含一类的常量信息,本文只探讨对于 Java 调用相关的常量池表。
CONSTANT_Utf8_info
字符串常量表,该表包含该类所使用的所有字符串常量,比如代码中的字符串引用、引用的类名、方法的名字、其他引用的类与方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至该表。
CONSTANT_Class_info
类信息表,包含任何被引用的类或接口的符号引用,每一个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。
CONSTANT_NameAndType_info
名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。
CONSTANT_InterfaceMethodref_info
接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。
CONSTANT_Methodref_info
类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引。
图 2. 常量池各表的关系
可以看到,给定任意一个方法的索引,在常量池中找到对应的条目后,可以得到该方法的类索引(class_index)和名字类型索引 (name_and_type_index), 进而得到该方法所属的类型信息和名称及描述符信息(参数,返回值等)。注意到所有的常量字符串都是存储在 CONSTANT_Utf8_info 中供其他表索引的。
也就是各种表格其实就是提供了一个索引。