第八章 多态
“我曾经被问到‘求教,Babbage先生,如果你向机器中输入错误的数字,可以得到正确的答案吗?’我无法恰当地理解产生这种问题的概念上地混淆” ——Charles Babbage(1791-1871)
再面向对象地程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过讲细节“私有化”把接口和实现分离开来。继承允许将对象视为它自己本身的类型或其基类类型来处理。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出来的。这种区别是根据方法行为的不同表现出来的,虽然这些方法都可以通过同一个基类来调用。
继承与多态:
继承:类与类之间的关系
多态:方法行为的不同表现
8.1 再论向上转型
对象既可以作为它自身的类型使用,也可以作为它的基类使用。
public enum Note{}class Obj{public void play(Note n){System.out.println("Obj.play()");}}class A extends Obj{public void play(Note n){System.out.println("A.play()");}}class M{public static void tune(Obj o){ // 提供向上转型方法o.play(Note);}public static void main(String[] args){A a = new A();tune(a); // 向上转型,将A转为Obj}}
M.tune()可以接受Obj类型,也可以接受任何导出自Obj的类。
8.1.1 忘记对象类型
忘记对象的类型,只记住它们的基类。
class B extends Obj{public void play(Note n){System.out.println("B.play()");}}class C extends Obj{public void play(Note n){System.out.println("C.play()");}}class M{public static void tune(A o){ // 为每一个新Obj导出类编写方法o.play(Note);}public static void tune(B o){o.play(Note);}public static void tune(C o){o.play(Note);}public static void main(String[] args){A a = new A();B b = new B();C c = new C();tune(a);tune(b);tune(c);}}
可以这样做,但有一个主要缺点:必须为每一个新Obj类编写特定代码。
8.2 转机
观察tune()方法:
public static void tune(Obj o){o.play(Note);}
接受一个Obj引用,编译器怎么知道这个Obj是A还是B?
8.2.1 方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。
上述程序疑问,主要是因为前期绑定。解决的办法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。就是说,编译器一直不知道类型,但是方法调用机制能找到正确的方法体,并加以调用。
Java中除了static方法和final方法(private属于final)之外,其他所有的方法都是后期绑定。通常我们不必判断,因为它会自动发生。
8.2.2 产生正确的形为
了解Java所有方法都是通过动态绑定实现多态之后,就可以只编写与基类打交道的程序代码了。
在编译时,编译器不需要获得任何特殊信息就能进行正确调用。
8.2.3 缺陷:“覆盖”私有方法
只有非private方法才能被覆盖,但是还需要注意覆盖private方法的现象,重写的private是全新的方法。
8.2.4 缺陷:域和静态方法
多态只适用于普通方法,不适用于类成员变量。
静态方法不具有多态性,静态方法是与类,而非与单个的对象相关联。
8.3 构造器和多态
尽管构造器并不具有多态性(实际上是隐式static方法),但了解构造器的运作有助于避免一些困扰。
8.3.1 构造器的调用顺序
基类构造器总是在导出类构造过程中被调用,而且按照继承层次逐渐向上,因为构造器有一项特殊任务:检查对象是否被正确构造。导出类只能访问自己的成员,基类成员通常是private,只有基类构造器才能恰当的对自己的元素进行初始化。因此必须另所有构造器都得到调用,否则就不可能正确构建完整对象。
构造器调用顺序:
- 调用基类构造器。逐层调用。
- 按声明顺序调用(基类)成员的初始化方法。
- 调用导出类构造器主体。
8.3.2 构造器内部的多态方法的形为
如果在一个构造器的内部调用正在构造的对象的某个动态绑定(重写)方法,那会发生什么情况?
当在基类构造器中调用导出类重写过的方法,实际运行的是导出类的方法,并且这个导出类的成员可能还未初始化。
构造器中唯一安全调用的方法是基类中final(final包括private)方法,因为这些方法不能被覆盖。
class A{void draw(){System.out.println("A.draw()");}A(){System.out.println("A() bdfore draw()");draw(); // 基类中多态调用,实际执行B.draw()方法System.out.println("A() after draw()");}}class B extends A{private int radius = 1;B(int r){radius = r;System.out.println("B.B(), radius = " + radius); //初始化完成,构造器赋值radius=5}void draw(){System.out.println("B.draw(), radius = " + radius);}}public class M{public static void main(String[] args){new B(5);}}
输出
A() bdfore draw()B.draw(), radius = 0 // 基类调用的实际是导出类多态方法,此时导出类成员还未初始化完成A() after draw()B.B(), radius = 5 // 此时整个初始化完成
前一节讲述的初始化顺序并不十分完整,初始化实际过程是:
- 在其他任何事务发生前,将分配给对象的储存空间初始化成二进制的零。
- 如前所述那样调用基类构造器,此时,调用覆盖后(多态)的方法,由于步骤1的关系,多态方法中用到的成员属性radius值为0;
- 按照声明的顺序调用成员的初始化方法。
- 调用导出类的构造器主体。
8.4 用继承进行设计
用继承表达行为间的差异,并用字段表达状态上的变化。
更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时,组合不会强制我们的程序设计进入继承的层次结构。
class O{public void act(){}}class A extends O{public void act(){System.out.println("A...");}}class B extends O{public void act(){System.out.println("B...");}}class S{// 组合private O o = new A();public void change(){o = new B();}public void performPlay(){o.act();}}public class M{public static void main(String[] args){S s = new S(); // S 成员初始化是 As.performPlay(); // S 调用默认是 As.change(); // S 修改状态为 Bs.performPlay(); // S 成员变成 B}}
输出
A...B...
通过继承得到两个不同的类,用于表达act()方法的差异;而S通过运用组合是自己的状态发生变化,这种情况下,这种状态的改变也产生了应为的改变
8.5 向下转型和运行时类型识别
Java中所有转型都会得到检查!
8.6 总结
多态意味着“不同的形式”。在面向对象程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式:不同版本的动态绑定方法。