第8章 多态
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。
1. 多态的定义和场景
多态方法调用允许一种类型表现出与相似类型的区别,只要这些类型派生自一个基类。这种区别是当你通过基类调用时,由方法的不同行为表现出来的。
所以,当参数是基类或者接口的类型时,实际传入派生类或者接口实现类的实例时,因为向上转型是绝对安全的,不会丢失任何属性。
2. 前期绑定
将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现),叫做前期绑定。
3. 后期绑定
后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。
Java 中除了 static 和 final 方法(private 方法也是隐式的 final)外,其他所有方法都是后期绑定。
所有的新类都可以和原有类正常运行,不需要改动 tune()方法。即使 tune() 方法单独存放在某个文件中,而且向 Instrument 接口中添加了新的方法,tune() 方法也无需再编译就能正确运行。下面是类图的实现:
特别注意,如果一个方法是static或者private的,那它就肯定不是多态的。
4. 构造器和多态
(1) 构造过程
在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为 private,你必须假定派生类只能访问自己的成员而不能访问基类的成员。只有基类的构造器拥有恰当的知识和权限来初始化自身的元素。因此,必须得调用所有构造器;否则就不能构造完整的对象。
对象的构造器调用顺序如下:
【1】基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
【2】按声明顺序初始化成员。
【3】调用派生类构造器的方法体。
(2)回收
子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 dispose() 方法(任意名称)。由于继承,如果有其他特殊的清理工作的话,就必须在派生类中重写 dispose() 方法。当重写 dispose() 方法时,记得调用基类的 dispose() 方法,否则基类的清理工作不会发生
(3)构造器中的动态绑定
如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些 bug 很隐蔽,难以发现。
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的 final 方法(这也适用于可被看作是 final 的 private 方法)。这些方法不能被重写,因此不会产生意想不到的结果。你可能无法永远遵循这条规范,但应该朝着它努力。
5. 使用继承设计
学习过多态之后,一切看似都可以被继承,因为多态是如此巧妙的工具。这会给设计带来负担。事实上,如果利用已有类创建新类首先选择继承的话,事情会变得莫名的复杂。
更好的方法是首先选择组合,特别是不知道该使用哪种方法时。组合不会强制设计是继承层次结构,而且组合更加灵活,因为可以动态地选择类型(因而选择相应的行为),而继承要求必须在编译时知道确切类型。
// polymorphism/Transmogrify.java // Dynamically changing the behavior of an object // via composition (the "State" design pattern) class Actor { public void act() {} } class HappyActor extends Actor { @Override public void act() { System.out.println("HappyActor"); } } class SadActor extends Actor { @Override public void act() { System.out.println("SadActor"); } } class Stage { private Actor actor = new HappyActor(); public void change() { actor = new SadActor(); } public void performPlay() { actor.act(); } } public class Transmogrify { public static void main(String[] args) { Stage stage = new Stage(); stage.performPlay(); stage.change(); stage.performPlay(); } } 输出: HappyActor SadActor
Stage 对象中包含了 Actor 引用,该引用被初始化为指向一个 HappyActor 对象,这意味着 performPlay() 会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对 SadActor 的引用,performPlay() 的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式)。与之相反,我们不能在运行时决定继承不同的对象,那在编译时就完全确定下来了。(这是组合与继承的差异)
有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。 在上个例子中,两者都用到了。通过继承得到的两个不同类在 act() 方法中表达了不同的行为,Stage 通过组合使自己的状态发生变化。这里状态的改变产生了行为的改变。