06_02 多态
1. 延迟方法(抽象方法)
- 如果方法在父类中定义,但并没有对其进行实现,那么我们称这个方法为延迟方法。
- 延迟方法有时也称为抽象方法,并且在C++语言中通常称之为纯虚方法。
- 延迟方法的一个优点就是可以使程序员在比实际对象的抽象层次更高的级别上考虑与之相关的活动。
- 延迟方法更具实际意义的原因:在静态类型面向对象语言中,对于给定对象,只有当编译器可以确认与给定消息选择器相匹配的响应方法时,才允许程序员发送消息给这个对象。
2. 多态变量
-
多态变量是指可以引用多种对象类型的变量。
-
这种变量在程序执行过程可以包含不同类型的数值。
-
对于动态类型语言,所有的变量都可能是多态的。
-
对于静态类型语言,多态变量则是替换原则的具体表现。
Parent variable=new Child();
使用多态实现的思路
- 编写父类
- 编写子类,子类重写(覆盖)父类方法
- 运行时,使用父类的类型,子类的对象
实现多态的两个要素:
- 方法重写
- 使用父类类型
多态变量形式
- 简单变量
- 接收器变量
- 向下造型(反多态)
- 纯多态(多态方法)
接收器变量
-
多态变量最常用的场合是作为一个数值,用来表示正在执行的方法内部的接收器。
-
伪变量 smalltalk:self,C++, Java,C#: this
纯多态( pure polymorphism )
-
实现多态函数的能力是面向对象编程最强大的技术之一
-
它支持代码只编写一次、高级别的抽象以及针对各种情况所需的代码裁剪。
-
通常,程序员都是通过给方法的接收器发送延迟消息来实现这种代码裁剪的
-
关于纯多态的一个简单实例就是用JAVA语言编写的StringBuffer类中的append方法。这个方法的参数声明为Object类型,因此可以表示任何对象类型。
class Stringbuffer{
String append(Object value){
return append(value.toString());}
…
}
方法toString被延迟实现。
- 方法toString在子类中得以重定义/重写。
- toString方法的各种不同版本产生不同的结果。
- 所以append方法也类似产生了各种不同的结果。
- append;一个定义,多种结果。
多态的运行机制
- Java多态机制是基于“方法绑定(binding)”,就是建立method call(方法调用)和method body(方法本体)的关联。如果绑定动作发生于程序执行前(由编译器和连接器完成),称为“先期绑定”。对于面向过程的语言它们没有其他选择,一定是先期绑定。比如C编译器只有一种method call,就是先期绑定。(C++有先期联编和后期联编)
- 当有多态的情况时,解决方案便是所谓的后期绑定(late binding):绑定动作将在执行期才根据对象型别而进行。后期绑定也被称为执行期绑定(run-time binding)或动态绑定(dynamic binding)。
- Java的所有方法,只有final,static,private和构造方法是前期绑定,其他都使用后期绑定。 将方法声明为final型可以有效防止他人覆写该函数。但是或许更重要的是,这么做可以“关闭”动态绑定。或者说,这么做便是告诉编译器:动态绑定是不需要的。于是编译器可以产生效率较佳的程序代码。
方法的动态绑定过程
- 编译器检查对象的声明类型和方法名。假设我们调用x.f(args)方法,并且x已经被声明为C类的变量,那么编译器会列举出C类中所有的名称为f的方法和从C类的超类继承过来的f方法
- 接下来编译器检查方法调用中提供的参数类型。如果在所有名称为f 的方法中有一个参数类型和调用提供的参数类型最为匹配,那么就确定调用这个方法( 重载解析)
- 当程序运行并且使用动态绑定调用方法时,虚拟机必须调用同x所指向的对象的实际类型相匹配的方法版本。假设实际类型为D(C的子类),如果D类定义了f(args)那么该方法被调用,否则就在D的超类中搜寻方法f(args),依次类推
- 对于静态类型面向对象编程语言,在编译时消息传递表达式的合法性(调用的合法性)不是基于接收器的当前动态数值,而是基于接收器的静态类来决定的。
- 运行时执行动态类所具有类型的方法
- 当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;
如果有,再去调用子类的该同名方法。
例子:
答案:
例子2:
3.泛型
- 泛型(Generic),是指具有在多种数据类型上皆可操作的含意 。即编写的代码可以在不同的数据类型上重用。
- 实现对源代码进行重用,既不是通过继承和聚合重用对象代码 ,也不是代码的复制粘贴复用。
- 泛型是具有占位符(类型参数)的类、结构、接口和方法,这些占位符是类、结构、接口和方法所存储或使用的一个或多个类型的占位符。
- 泛型,即“参数化类型”。即将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。
public class Generic<T> {
public T Field;}
C++中的泛型
- 在C++语言中称泛型为模板(template),模板由函数模板和类模板两部分组成
- 以所处理的数据类型的说明作为参数的函数叫做函数模板。
- 以所处理的数据类型的说明作为参数的类就叫类模板。
template <class Type>
template : 关键字,总是放在模板的定义与声明的最前面
<> : 模板参数列表,如果有多个模板参数用逗号隔开;模板参数分模板类型参数和模板非类型参数(代表一个常量表达式)。
class:关键字, 声明模板类型参数,用typename替代也可以。
template <class Type , class TypeB>
Type Add( Type a, TypeB b )
{
return a + b ;
}
注意此时a和b是两种不同的变量类型
template <class Type , int size>
Type Add( Type a)
{
return a + size ;
}
注意此时a是模板类型参数,size是模板非类型参数。
template <class Type >
Type Add( Type a , int size)
{
return a + size ;
}
注意此时a是模板类型参数,而size是函数参数
模板函数的实例化
用系统实际的内置或用户定义类型来替换模板的类型参数。
注意:
- 不论是内置类型还是用户自定义类型必须要支持模板函数内的操作。
Java中的泛型
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
};
public class GenericsDemo06{
public static void main(String args[]){
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
p.setVar("it") ; // 设置字符串
System.out.println(p.getVar().length()) ; // 取得字符串的长度
}
};
- 使用了泛型的类class
- 创建类对象时,指明具体的类型
- 根据需求返回不同的类型
Java泛型接口
泛型方法
- 方法独立于类而产生变化。
- 影响范围小,可以用于static方法。
泛型的好处:
- 代码的复用
- 减少类型转换
集合框架中的接口(疑惑)
- 所谓框架就是类库的集合。集合框架就是一个用来表示和操作集合的统一的架构,包含了实现集合的接口与类。
使用泛型和未使用泛型的比较
类型参数_比较非泛型和泛型ArrayList类
当声明或者实例化一个泛型的对象时,必须指定类型参数的值:
Map<String, String> map = new HashMap<String, String>();
泛型原理
- Java泛型是在编译器的层面上实现的
- 在编译后,通过擦除,将泛型的痕迹全部抹去。
- 擦除:将任何具体的类型信息都消除,唯一知道的就是正在使用一个对象
- JVM不知道泛型的存在
泛型的好处(疑惑)
- 避免由于数据类型的不同导致方法或类的重载。
- 类型安全。 泛型的一个主要目标就是提高程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
- 消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。(为什么?)
- 多态的另一种表现形式。(参量多态)
4. 多重继承
- 当我们观察现实世界中的对象分类时发现,这些对象很少会符合我们所构造的单一继承关系。
- 现实世界中的对象几乎总是以一种多重、互相不重叠的方式进行分类的
可以看成是单继承的扩展
- 多重继承:一个对象可以有两个或更多不同的父类,并可以继承每个父类的数据和行为。
- 派生的分类对每个父类仍然符合“是一个”规则,或“作为一个”关系。
- 同时扮演多个角色。
多重继承中存在的问题:名字冲突
- 在多重继承下,若多个基类具有相同的成员名,可能造成对基类中该成员的访问出现不是唯一的情况,则称为对基类成员访问的二义性。
消除二义性的两种方法:
- 使用相应的类名来标识
device.Device1::showPower();
不够理想:
- 语法上与其他的函数调用语法不同
- 程序员必须记住哪个方法来自于哪个类
- 在派生类中重定义有名称冲突的成员
void showPower()
{
Device1::showPower();
Device2::showPower();
}
-
子类继承这些新的父类,覆盖/重写相应的新方法。
-
当独立使用子类时,新的子类对两个行为都可以访问
-
当以替换的方式对该对象赋值给任何一个父类的实例时,都会产生所希望的行为。
-
Java,C#语言都不支持类的多重继承,但它们都支持接口的多重继承。
-
对于子类来说,接口不为其提供任何代码,所以不会产生两部分继承代码之间的冲突。
END