目录
一、向上造型
二、重载与重写
三、方法重写后的动态绑定
四、JVM的结构与常量池
五、java的方法调用方式
六、方法表与方法调用
七、接口调用
八、多态的依附性
Think in java(version 4) P148:
多态通过分离做什么和怎么做,从另一角度将借口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和显示分离开来。而多态的作用则是消除类型之间的耦合关系。继承允许将对象视为自己本身的类型或基类型来加以处理。这种能力极为重要。因为它允许将多种类型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些不同类型上了。多态方法调用允许同一种类型表现出与其他相似类型之间的区别,这种区别是根据方法行为的不同而表现出来的,虽然这些方法都可以通过同一个基类来调用。
Luca Cardelli和Peter Wegner("On Understanding Types, Data Abstraction, and Polymorphism"一文的作者)把多态分为两大类:特定的和通用的,四小类:强制的,重载的,参数的和包含的。他们的结构如下:
强制的:一种隐式做类型转换的方法。
强制多态隐式的将参数按某种方法,转换成编译器认为正确的类型以避免错误
比如在表达式1.0+1中将double类型和int类型相加,Java中没有明确定义这种运算。不过,编译器隐式的将第二个操作数转换为double型,并作double型的加法。做对程序员来说十分方便,否则将会抛出一个编译错误,或者强制程序员显式的将 int转换为double
同样强制的多态也会发生在方法调用中,在之前向上造型的例子中tune()方法传递的是Wind引用,编译器隐式地将flute对象转化为Instrument类型的对象。
重载的:将一个标志符用作多个意义。
详情见下文
参数的:为不同类型的参数提供相同的操作。
参数多态允许把许多类型抽象成单一的表示。例如,List 抽象类中,描述了一组具有同样特征的对象,提供了一个通用的模板。你可以通过指定一种类型以重用这个抽象类。这些参数可以是任何用户定义的类型,大量的用 户可以使用这个抽象类,因此参数多态毫无疑问的成为最强大的多态。
包含的:类包含关系的抽象操作。
包含多态通过值的类型和集合的包含关系实现了多态的行为.在包括Java在内的众多面向对象语言中,包含关系是子类型的。所以,Java的包含多态是子 类型的多态。
一、什么是向上造型
在Think in java早期版本中把“引用”称为“句柄”,其实用“句柄”能更好地理解引用与对象之间的关系
尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“句柄”(Handle)。在其他Java参考书里,还可看到有的人将其称作一个“引用”,甚至一个“指针”。可将这一情形想象成用遥控板(句柄)操纵电视机(对象)。只要握住这个遥控板,就相当于掌握了与电视机连接的通道。但一旦需要“换频道”或者“关小声音”,我们实际操纵的是遥控板(句柄),再由遥控板自己操纵电视机(对象)。如果要在房间里四处走走,并想保持对电视机的控制,那么手上拿着的是遥控板,而非电视机。
举个例子:空调遥控器。通常情况下同个品牌的不同型号的遥控器是可以通用的,比如现在有一个2010年产的空调A(父类)的遥控器和2018年产的空调B(子类),空调B是空调A的改进版(拥有A的所有功能以及一些新的功能) ,此时可以把这个遥控器看成是一个“句柄”(引用),这个遥控器来控制空调B的过程(父类的引用指向子类对象)即为“向上造型”,当然此时只能遥控空调A所具有的功能,如果同一个功能被改进(重写)了就表现出改进后的效果。
假如有一个称为Instrument(乐器)的父类和一个称为Wind(管乐)的子类
class Instrument{
public void paly(){}
static void tune(Instrument i){
i.play();
}
}
public class Wind extends Instrument{
public static void main(String[] args){
Wind flute=new Wind();
Instrument.tune(flute);
}
}
由于继承可以使父类中所有的方法在子类中也同样有效,所以能够向父类发送的所有信息同样也可以向子类发送。如有Instrument中有一个paly()弹奏方法,那么Wind中也同样具有。而tune()方法可以接受所有Instrument引用,在main方法中传递给tune()方法的就是一个Wind引用,也就是说java编译器把Wind对象看成是Instrument类型的对象,这一将Wind引用转换为Instrument引用的动作也是向上造型。
二、为什么叫“向上造型”
在传统的类继承图中,根置于页面的顶端,然后逐渐向下。Wind.java的继承图是这样的:
可见由子类转型为父类,在继承图上是向上移动的,因此称为向上造型。由于向上造型时从一个较专用的类型向通用类型转换,所以这个过程是很安全的,子类可能比父类含有更多的方法,但它必须至少具备父类中所含的所有方法。在向上造型的过程中,类接口中唯一会发生的事情是丢失方法,而不是获取它们。这也是为什么向下造型时含有一个难题。
三、为什么要向上造型
在包中单独建立一个乐府Note类
public enum Note{
MIDDLE_C,C_SHARP,B_FLAT;
}
Instrument类
class Instrument{
public void play(Note n){
print("Instrument.play()");
}
}
Wind类
public class Wind extends Instrument{
public void play(Note n){
System.out.println("Wind.play()"+n);
}
}
public class Music{
public static void tune(Instrument i){
i.play(Note.MIDDLE_C);
}
public static void main(String[] args){
Wind flute=new Wind();
tune(flute);
}
}
输出为 Wind.play() MIDDLE_C
在Music.java中,Wind引用传递到tune()方法时发生了向上转型,编译器“忘记”了flute的对象类型,那么为什么不直接让tune()方法接受一个Wind引用作为自己的参数呢,这样似乎会更加直观。但是这样引发了一个重要的问题:如果那样做,就要为系统内Instrument的每种类型都编写一个新的tune()方法,这意味着需要更多的编程,如果以后想添加类似tune()的新方法,或者添加自Instrument到处的新子类仍需要大量工作。此外如果我们忘记重载某个方法,编译器不会返回任何错误信息。
而多态就允许只写一个简单方法作仅接受父类作为参数,而不是特殊的子类。也就是说我们不管子类的存在编写的代码只是与父类打交道。
四、向上转型的特点
(1)成员变量:编译看左边,运行看左边
(2)成员方法:编译看左边,运行看右边 (动态绑定)
(3)静态方法:编译看左边,运行看左边
一. 成员变量:编译看左边,运行看左边
class Father {
int num = 10;
}
class Son extends Father {
int num = 20;
}
public class Polymorphic {
public static void main(String[] args) {
Father f = new Son();
System.out.println(f.num);
Son s = new Son();
System.out.println(s.num);
}
}
首先类加载机制加载Demo2_Polymorphic.class,主方法进栈。执行Father f创建变量时,开始加载Father.class和Son.class两个类。创建Son实例分配内存时,内存区域有一块super,也就是子类可以访问父类的内存区域,num初始化值为10(super指针指向);它自己内存区域有num初始化值为20(this指针指向),此时的f是指向Son实例内存的super内存块,因此f.num的取值肯定就是10了。
二. 成员方法:编译看左边,运行看右边
class Father {
int num = 10;
public void print() {
System.out.println("father");
}
}
class Son extends Father {
int num = 20;
public void print() {
System.out.println("son");
}
}
public class Demo2_Polymorphic {
public static void main(String[] args) {
Father f = new Son();
f.print();
}
}
在调用f.print()方法时,编译器编译首先检查父类Father中有木有print()方法(蓝线),如果没有就会编译报错;如果有就通过编译开始运行print()方法,运行看右边也就是运行的子类Son的print方法(红线),此时子类的print方法进栈运行,结束后弹栈。
三. 静态方法:编译看左边,运行看左边
class Father {
int num = 10;
public static void print() {
System.out.println("static father");
}
}
class Son extends Father {
int num = 20;
public static void print() {
System.out.println("static son");
}
}
public class Demo2_Polymorphic {
public static void main(String[] args) {
Father f = new Son();
f.print();
}
}
静态方法就很好理解了,因为是static修饰,随着类的加载里面的变量和方法就是类的属性和方法,f.print(),其实也就是Father.print(),很显然调用的就是父类的方法。
Java的方法重载,就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。调用方法时通过传递的参数类型来决定具体使用哪个方法,这就是多态性。
Java的方法重写,是父类与子类之间的多态性,子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改。
多态允许具体访问时实现方法的动态绑定。Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。
继承:在执行某个方法时,在方法区中找到该类的方法表,再确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时会按照继承关系搜索父类的方法表中该偏移量对应的方法。
接口:Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同一个接口的的方法在不同类方法表中的位置就可能不一样了。所以不能通过偏移量的方法,而是通过搜索完整的方法表。
当程序运行需要某个类时,类加载器会将相应的class文件载入到JVM中,并在方法区建立该类的类型信息,这个类型信息就存贮在方法区。类型信息一般包括该类的方法代码、类变量、成员变量的定义等等。可以说,类型信息就是类的 Java 文件在运行时的内部结构,包含了改类的所有在 Java 文件中定义的信息。
注意,这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以方法区是各个线程共享的内存区域),而在堆中可以有多个该class对象。可以通过堆中的class对象访问到方法区中类型信息。就像在java反射机制那样,通过class对象可以访问到该类的所有信息一样。
Java 的方法调用有两类,动态方法调用与静态方法调用。静态方法调用是指对于类的静态方法的调用方式,是静态绑定的;而动态方法调用需要有方法调用所作用的对象,是动态绑定的。
JVM 的方法调用指令有四个,分别是 invokestatic,invokespecial,invokesvirtual 和 invokeinterface。前两个是静态绑定,后两个是动态绑定的。
类调用 (invokestatic) 是在编译时就已经确定好具体调用方法的情况。
实例调用 (invokevirtual)则是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。
常量池
常量池中保存的是一个 Java 类引用的一些常量信息,包含一些字符串常量及对于类的符号引用信息等。Java 代码编译生成的类文件中的常量池是静态常量池,当类被载入到虚拟机内部的时候,在内存中产生类的常量池叫运行时常量池。
常量池在逻辑上可以分成多个表:
1、CONSTANT_Utf8_info
字符串常量表,该表包含该类所使用的所有字符串常量,比如代码中的字符串引用、引用的类名、方法的名字、其他引用的类与方法的字符串描述等等。其余常量池表中所涉及到的任何常量字符串都被索引至该表。
2、CONSTANT_Class_info
类信息表,包含任何被引用的类或接口的符号引用,每一个条目主要包含一个索引,指向 CONSTANT_Utf8_info 表,表示该类或接口的全限定名。
3、CONSTANT_NameAndType_info
名字类型表,包含引用的任意方法或字段的名称和描述符信息在字符串常量表中的索引。
4、CONSTANT_InterfaceMethodref_info
接口方法引用表,包含引用的任何接口方法的描述信息,主要包括类信息索引和名字类型索引。
5、CONSTANT_Methodref_info
类方法引用表,包含引用的任何类型方法的描述信息,主要包括类信息索引和名字类型索引。
可以看到,给定任意一个方法的索引,在常量池中找到对应的条目后,可以得到该方法的类索引(class_index)和名字类型索引 (name_and_type_index), 进而得到该方法所属的类型信息和名称及描述符信息(参数,返回值等)。注意到所有的常量字符串都是存储在 CONSTANT_Utf8_info 中供其他表索引的。
方法表是实现动态调用的核心。为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,该指针指向记录该类方法的方法表,方法表中的每一个项都是对应方法的指针。这些方法中包括从父类继承的所有方法以及自身重写(override)的方法。
如有类定义Animal, Cat, Dog
class Animal {
public String toString(){
return "I'm an Animal.";
}
public void eat(){}
public void noise(){}
}
class Cat extends Animal{
public String toString(){
return "I'm a cat";
}
public void noise(){}
public void dayDreaming(){}
}
class Dog extends Animal{
public String toString(){
return "I'm a dog";
}
public void noise(){}
public void watchOver(){}
}
当这三个类被载入到 Java 虚拟机之后,方法区中就包含了各自的类的信息。Cat 和 Dog 在方法区中的方法表可表示如下:
可以看到,Cat 和 Dog 的方法表包含继承自 Object 的方法,继承自直接父类 Animal 的方法及各自新定义的方法。注意方法表条目指向的具体的方法地址,如 Cat 继承自 Object 的方法中,只有 toString() 指向自己的实现(Cat 的方法代码),其余皆指向 Object 的方法代码;其继承自于 Animal 的方法 eat() 和 noise() 分别指向 Animal 的方法实现和本身的实现。
如果子类改写了父类的方法,那么子类和父类的那些同名的方法共享一个方法表项。
因此,方法表的偏移量总是固定的。所有继承父类的子类的方法表中,其父类所定义的方法的偏移量也总是一个定值。
Animal 或 Object中的任意一个方法,在它们的方法表和其子类 Cat 和 Dog 的方法表中的位置 (index) 是一样的。这样 JVM 在调用实例方法其实只需要指定调用方法表中的第几个方法即可。
class House{
void catNDog{
Animal cat=new Cat();
cat.noise();
}
}
当编译 House 类的时候,生成 cat.noise()的方法调用假设为:
Invokevirtual #12
设该调用代码对应着 cat.noise(); #12 是 House 类的常量池的索引。JVM执行该调用指令的过程如下所示:
(1)在常量池中找到方法调用的符号引用。
(2)查看Animal的方法表,得到noise方法在该方法表的偏移量(假设为15),这样就得到该方法的直接引用。
(3)根据this指针得到具体的对象(即 cat 所指向的位于堆中的对象)。
(4)根据对象得到该对象对应的方法表,根据偏移量15查看有无重写(override)该方法,如果重写,则可以直接调用(Cat的方法表的noise项指向自身的方法而非父类);如果没有重写,则需要拿到按照继承关系从下往上的基类(这里是Animal类)的方法表,同样按照这个偏移量15查看有无该方法。
Java 允许一个类实现多个接口,从某种意义上来说相当于多继承,这样同样的方法在基类和派生类的方法表的位置可能会不一样
interface IDance{
void dance();
}
class Person {
public String toString(){
return "I'm a person.";
}
public void eat(){}
public void speak(){}
}
class Dancer extends Person implements IDance {
public String toString(){
return "I'm a dancer.";
}
public void dance(){}
}
class Snake implements IDance{
public String toString(){
return "A snake."; }
public void dance(){
//snake dance
}
}
由于接口的介入,继承自接口 IDance 的方法 dance()在类 Dancer 和 Snake 的方法表中的位置已经不一样了,显然我们无法仅根据偏移量来进行方法的调用。Java对于接口方法的调用是采用搜索方法表的方式,如,要在Dancer的方法表中找到dance()方法,必须搜索Dancer的整个方法表。因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的。
模型中包含5种类型,4个类和一个接口:
一、多个引用依附于一个对象
1、Derived2 derived2 = new Derived2();
图2 :Derived2 对象上的引用
声明了的derived2这个对象是Derived2类的。上图中的最顶层把Derived2引用描述成一个集合的窗口,虽然其下的Derived2对象是可见的。这里为每个Derived2类型的操作留了一个孔。Derived2对象的每个操作都去映射适当的代码,按照上面的代码所描述的那样。例如,Derived2对象映射了在Derived中定义的m1()方法。而且还重载了Base类的m1()方法。一个Derived2的引用变量无权访问Base类中被重载的m1()方法。但这并不意味着不可以用super.m1()的方法调用去使用这个方法。Derived2的其他的操作映射同样表明了每种类型操作的代码执行。
既然你有一个Derived2对象,可以用任何一个Derived2类型的变量去引用它。如图1所示,Derived, Base和IType都是Derived2的基类。所以,Base类的引用是很有用的。图3描述了以下语句的概念观点。
2、Base base = derived2;
图3:Base类引用附于Derived2对象之上
虽然Base类的引用不用再访问m3()和m4(),但是却不会改变它Derived2对象的任何特征及操作映射。无论是变量derived2还是 base,其调用m1()或m2(String)所执行的代码都是一样的。
两个引用之所以调用同一个行为,是因为Derived2对象并不知道去调用哪个方法。对象只知道什么时候调用,它随着继承实现的顺序去执行。这样的顺序决定了Derived2对象调用Derived里的m1()方法,并调用Derived2里的m2(String)方法。这种结果取决于对象本身的类型,而不是引用的类型。
尽管如此,但不意味着你用derived2和base引用的效果是完全一样的。如图3所示,Base的引用只能看到Base类型拥有的操作。所以,虽然Derived2有对方法m3()和m4()的映射,但是变量base不能访问这些方法。
运行期的Derived2对象保持了接受m3()和m4()方法的能力。类型的限制使Base的引用不能在编译期调用这些方法。编译期的类型检查像一套铠甲,保证了运行期对象只能和正确的操作进行相互作用。换句话说,类型定义了对象间相互作用的边界
以上两个例子是把两个及两个以上的引用依附于一个对象。虽然Derived2对象在被依附之后仍保持了变量的类型,但是,图3中的Base类型的引用依附之后,其功能减少了。结论很明显:把一个基类的引用依附于派生类的对象之上会减少其能力。
假设有一个名为ref的引用依附于一个包含如下方法的类的对象,用一个Derived2的参数调用poly(Base)是符合参数类型检查的:方法调用把一个本地Base类型的变量依附在一个引入的对象上。所以,虽然这个方法只接受Base类型的参数,但Derived2对象仍是允许的。开发这就不必选择丢失功能的方案。从人眼在通过Derived2对象时所看到的情况,Base类型引用的依附导致了功能的丧失。但从执行的观点看,每一个传入poly1(Base)的参数都认为是Base的对象。执行机并不在乎有多个引用指向同一个对象,它只注重把指向另一个对象的引用传给方法。这些对象的类型不一致并不是主要问题。执行器只关心给运行时的对象找到适当的实现。
二、附于多个对象的引用
poly1(Base)的实现代码是调用传进来的参数的m1()方法。下图展示了把三个类的对象传给方法时,面向类型的所使用的体系结构。
图4:将Base引用指向Derived类,以及Base对象
图3中,m1()调用了Derived类的代码;上面代码中的注释标明了ploy1(Base)调用Derived.m1()。图4中Derived对象调用的仍然是Derived类的m1()方法。最后,图4中,Base对象调用的m1()是Base类中定义的代码。
多态的魅力何在?再来看一下poly1(Base)的代码,它可以接受任何属于Base类范畴的参数。然而,当他收到一个Derived2的对象时,它实际上却调用了Derived版本的方法。当你根据Base类派生出其他类时,如Derived,Derived2,poly1(Base)都可以接受这些参数,并作出选择调用合适的方法。多态允许你在完成poly1(Base)后扩展它的用途。
这看起来当然很神奇。基本的理解展示了多态的内部工作原理。在面向类型的观点中,底层的对象所实现的代码是非实质性的。重要的是,类型检查器会在编译期间为每个引用选择合适的代码以实现其方法。多态使开发者运用面向类型的观点,不考虑实现的细节。这样有助于把类型和实现分离(实际用处是把接口和实现分离)。
参考资料
https://blog.csdn.net/SEU_Calvin/article/details/52191321
https://blog.csdn.net/huangrunqing/article/details/51996424
https://blog.csdn.net/u011860731/article/details/48731533
https://blog.csdn.net/qq_25474469/article/details/79874576
Java编程思想(第四版)