在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征.
1. 再论向上转型
对象既可以作为它自己本身的类型使用,也可以作为它的基类使用.而这种把对某个对象的引用视为对其基类的引用的做法被称为”向上转型” – 因为在继承书的画法中,基类是放置在上方的.
首先,既然几个例子都要演奏乐符 (Note) ,我们就应该在包中单独创建一个Note类.
|
|
在这里,Wind是一种Instrument,因此可以从Instrument类继承.
|
|
|
|
|
|
Music.tune()
方法接受一个Instrument引用,同时也接受任何导出自Instrument 的类.在main()方法中,当一个Wind引用传递到tune()方法时,就会出现这种情况,而不需要任何类型转换.因为Wind到Instrument继承而来,所以Instrument的接口必定存在于Wind中.从Wind向上转型到Instrument可能会缩小接口,但不会比Instrument的全部接口更窄.
1.1 忘记对象类型
如果我们不管导出类的存在,编写的代码只是与基类打交道,会不会更好呢?
2. 转机
2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定.若在程序执行前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定.
后期绑定的含义就是在运行时根据对象的类型进行绑定.后期绑定也叫做动态绑定或运行时绑定.如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法.也就说说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用.
Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定.
对某个方法声明final,它可以防止其他人覆盖该方法.但更重要的一点是告诉编译器不需要对其进行动态绑定.这样,编译器就可以为final方法调用生成更有效的代码.
2.2 产生正确的行为
发送消息给某个对象时,让该对象去判定应该做什么事.
向上转型可以像下面这条语句那么简单:
Shape s = new Circle();
这里,创建了一个Circle对象,并把得到的引用立即赋值给Shape,因为通过继承.Circle就是一种Shape.
假设你调用一个基类方法(它已在导出类中被覆盖)
s.draw();
由于后期绑定(多态),所以调用的是Circle.draw()方法.
|
|
|
|
|
|
|
|
|
|
Shape基类为自它哪里继承而来的所有导出类建立了一个公用接口,所有形状都可以描绘和擦除.导出类通过覆盖这些定义,来为每种特殊类型的几何形状提供单独的行为.
RandomShapeGenerator是一种”工厂”,在我们每次调用next()方法时,它可以随机选择Shape对象产生一个引用.向上转型是在return语句里产生的.每个return语句取得一个指向某个Cricle,Square或者Triangle的引用,并将其以Shape类型从next()方法中发送出去.所以无论我们在什么时候调用next()方法,都不会知道具体类型到底是什么,因为我们总是只能获得一个通用的Shape引用.
2.3 可扩展性
因为可以从通用的基类继承出新的数据类型,从而新添加一些功能.那些操纵基类接口的方法不需要任何改动就可以应用于新类.
|
|
新添加的方法what()返回一个带有类型描述的string引用;另一个新添加的方法adjust()则提供每种乐其的调用方法.
在main()中,当我们将某种引用置入orchestra 数组中,就会自动向上转型到Instrument.
多态是一项让程序员”将改变的事物与未知的事物分离开来”的重要技术.
2.4 缺陷 : “覆盖”私有方法
|
|
我们所期待的是输出public f(),但由于private方法被自动认为是final方法,而且对导出类是屏蔽的.因此,在这种情况下,Derived类中的f()方法就是一个全新的方法;既然基类的f()方法在子类中不可见,因此也不能被重载.
确切地说,在导出类中,对于基类的private方法,最好采用不同的名字.
2.5 缺陷 : 域与静态方法
如果你直接访问某个域,这个访问就将会在编译器进行解析.
|
|
当Sub对象转型为Super引用时,任何域访问都将由编译器解析,因此不是多态.在本例中,为Super.field和Sub.field分配了不同的存储空间.这样,Sub实际上包含两个称为field的域:它自己的和它从Super处得到的.然而,在引用Sub中的field时所产生的默认域并非Super版本的field域.因此,为了得到Super.field,必须显式地指明super.field.
如果某个方法是静态的,它的行为就不具有多态性,静态方法是与类,而并非与单个的对象相关联.
3. 构造器和多态
构造器并不具有多态性(它们实际上是static方法,只不过static声明是隐式的).
3.1 构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,已使每个基类的构造器都能得到调用.因为构造器具有一项特殊的任务:检查对象是否被正确地构造.导出类只能访问它自己的成员,不能访问基类中的成员(基类的成员通常是private类型).只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化.因此,必须令所有构造器都得到调用.这正是编译器为什么要强制每个导出类部分都必须调用构造器的原因.在导出类的构造器主体中,如果没有明确指定调用某个基类构造器,它就会默默地调用默认构造器.如果没有明确构造器,编译器就会报错!
|
|
用其他类创建了一个复杂的类,并且每个类都有一个声明它自己的构造器,其中最重要的类时Sandwich,它反映了三层继承(若将自Object的隐含继承地也算在内,就是四层)以及三个成员对象,当在main()里创建一个Sandwich对象后,就可以看到输出的结果.
- 调用基类构造器.这个步骤还不断地反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层的导出类.
- 按声明顺序调用成员的初始化方法.
- 调用导出类构造器的主体.
3.2 继承和清理
通过组合和继承方法来创建新类时,永远不必担心对象的清理问题.子对象通过都会留给垃圾回收器进行处理.如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法.并且由于继承的缘故,如果我们有其他作为垃圾回收器一部分的特殊清理功能,就必须在导出类中覆盖dispose()方法.当覆盖被继承的dispose()方法适,务必记住调用基类版本dispose()方法;否则,基类的清理动作就不会发生.
3.3 构造器内部的多态方法的行为
|
|
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零.
- 如前所述那有调用基类构造器.此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤1的缘故,我们此时会发现radius的值为0.
- 按照声明的顺序调用成员的初始化方法.
- 调用导出类的构造器主体.
因此,在编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常的状态,如果也可以的情况,避免调用其它方法.
4. 协变返回类型
它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型:
5. 用继承进行设计
用继承表达行为间的差异,并用子弹表达状态上的变化.
5.1 纯继承与扩展
这种被称作为纯粹的”is-a”关系,因为一个类的接口已经确定了它应该是什么,继承可以确保所有的导出类具有基类的接口,且绝对不会少.
5.2 向下转型与运行时类型识别
由于向上转型(在继承层中向上移动)会丢失具体的类型信息.所以,我们通过向下转型(也就是在继承层次中向下移动),然而,我们知道向上转型是安全的,因为基类不会具有大于导出类的接口.因此,我们通过基类接口发送的信息保证都能被接受.但是对于向下转型,例如:我们无法知道一个”几何形状”它确实是一个”圆”还是”别的三角形”.
在java中,所有的转型都会被检查,以便确保它的确是我们希望的那种类型.如果不是.则返回ClassCastException(类转型异常)
6. 总结
多态意味着”不同的形式”,在面向对象的程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法.