在面向对象的程序设计语言中 多态是继数据抽象和继承之后的第三种基本特征
再论向上转型
对象既可以作为它自己本身的类型使用 也可以作为它的基类型使用 这种把对某个对象的引用视为对其基类型的引用的做法被称作向上转型——因为在继承树的画法中 基类是放置在上方的
转机
方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定 若在程序执行前进行绑定(如果有的话 由编译器和连接程序实现) 叫做前期绑定
当编译器只有一个Instrument引用时 它无法知道究竟调用哪个方法才对 解决的办法就是后期绑定 它的含义就是在运行时根据对象的类型进行绑定 后期绑定也叫做动态绑定或运行时绑定
Java中除了static方法和final方法(private方法属于final方法)之外 其他所有的方法都是后期绑定 这意味着通常情况下 我们不必判定是否应该进行后期绑定——它会自动发生
产生正确的行为
在编译时 编译器不需要获得任何特殊信息就能进行正确的调用
可扩展性
由于有多态机制 我们可根据自己的需求对系统添加任意多的新类型 而不需更改tune()方法 在一个设计良好的OOP程序中 大多数或者所有方法都会遵循tune()的模型 而且只与基类接口通信 这样的程序是可扩展的 因为可以从通用的基类继承出新的数据类型 从而新添一些功能 那些操纵基类接口的方法不需要任何改动就可以应用于新类
事实上 不需要改动tune()方法 所有的新类都能与原有类一起正确运行 即使tune()方法是单独存放在某个文件中 并且在Instrument接口中添加了其他的新方法 tune()也不需再编译就能正确运行
可以看到 tune()方法完全可以忽略它周围代码所发生的全部变化 依旧正常运行 这正是我们期望多态所具有的特性 我们所做的代码修改 不会对程序中其他不应受到影响的部分产生破坏 换句话说 多态是一项让程序员 将改变的事物与未变的事物分离开来 的重要技术
缺陷:覆盖 私有方法
由于private方法被自动认为是final方法 而且对导出类是屏蔽的 在这种情况下 Derived类中的f()方法就是一个全新的方法 既然基类中的f()方法在子类Derived中不可见 因此甚至也不能被重载
只有非private方法才可以被覆盖 但是还需要密切注意覆盖private方法的现象 这时虽然编译器不会报错 但是也不会按照我们所期望的来执行 确切地说 在导出类中 对于基类中的private方法 最好采用不同的名字
缺陷:域与静态方法
只有普通的方法调用可以是多态的 如果你直接访问某个域 这个访问就将在编译期进行解析
如果某个方法是静态的 它的行为就不具有多态性
静态方法是与类 而并非与单个的对象相关联的
构造器和多态
构造器并不具有多态性(它们实际上是static方法 只不过该static声明是隐式的)
继承与清理
通过组合和继承方法来创建新类时 永远不必担心对象的清理问题 子对象通常都会留给垃圾回收器进行处理 如果确实遇到清理的问题 那么必须用心为新类创建dispose()方法 并且由于继承的缘故 如果我们有其他作为垃圾回收一部分的特殊清理动作 就必须在导出类中覆盖dispose()方法 当覆盖被继承类的dispose()方法时 务必记住调用基类版本dispose()方法 否则 基类的清理动作就不会发生
万一某个子对象要依赖于其他对象 销毁的顺序应该和初始化顺序相反 对于字段 则意味着与声明的顺序相反(因为字段的初始化是按照声明的顺序进行的) 对于基类 应该首先对其导出类就行清理 然后才是基类
在上面的示例中还应该注意到 Frog对象拥有其自己的成员对象 Frog对象创建了它自己的成员对象 并且知道它们应该存活多久(只要Frog存活着) 因此Frog对象知道何时调用dispose()去释放其成员对象 然而 如果这些成员对象中存在于其他一个或多个对象共享的情况 问题就变得更加复杂了 你就不能简单地假设你可以调用dispose()了 在这种情况下 也许就必需使用引用计数来跟踪仍旧访问着共享对象的对象数量了
构造器内部的多态方法的行为
如果要调用构造器内部的一个动态绑定方法 就要用到那个方法的被覆盖后的定义 然而 这个调用的效果可能相当难于预料 因为被覆盖的方法在对象被完全构造之前就会被调用 这可能会造成一些难于发现的隐藏错误
初始化的实际过程是:
编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入正常状态 如果可以的话 避免调用其他方法
协变返回类型
协变返回类型表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型
与Java较早版本之间的主要差异就是较早的版本将强制process()的覆盖版本必须返回Grain 而不能返回Wheat 尽管Wheat是从Grain导出的 因而也应该是一种合法的返回类型 协变返回类型允许返回更具体的Wheat类型
用继承进行设计
当我们使用现成的类来建立新类时 如果首先考虑使用继承技术 反倒会加重我们的设计负担 使事情变得不必要地复杂起来
更好的方式是首先选择 组合 尤其是不能十分确定应该使用哪一种方式时 组合不会强制我们的程序设计进入继承的层次结构中 而且 组合更加灵活 因为它可以动态选择类型(因此也就选择了行为) 相反 继承在编译时就需要知道确切类型
我们在运行期间获得了动态灵活性(这也称作状态模式) 与此相反 我们不能在运行期间决定继承不同的对象 因为它要求在编译期间完全确定下来
一条通用的准则是:用继承表达行为间的差异 并用字段表达状态上的变化 在上述例子中 两者都用到了:通过继承得到了两个不同的类 用于表达act()方法的差异 而Stage通过运用组合使自己的状态发生变化 在这种情况下 这种状态的改变也就产生了行为的改变
纯继承与扩展
采取 纯粹 的方式来创建继承层次结构似乎是最好的方式 也就是说 只有在基类中已经建立的方法才可以在导出类中被覆盖
向下转型与运行时类型识别
由于向上转型(在继承层次中向上移动)会丢失具体的类型信息 所以我们就想 通过向下转型——也就是在继承层次中向下移动——应该能够获取类型信息
在Java语言中 所有转型都会得到检查 所以即使我们只是进行一次普通的加括弧形式的类型转换 在进入运行期时仍然会对其进行检查 以便保证它的确是我们希望的那种类型 如果不是 就会返回一个ClassCastException(类转型异常) 这种在运行期间对类型进行检查的行为称作 运行时类型识别(RTTI)
总结
多态意味着 不同的形式 在面向对象的程序设计中 我们持有从基类继承而来的相同接口 以及使用该接口的不同形式:不同版本的动态绑定方法
如果不运用数据抽象和继承 就不可能理解或者甚至不可能创建多态的例子 多态是一种不能单独来看待的特性(例如 像switch语句是可以的) 相反它只能作为类关系 全景 中的一部分 与其他特性协同工作
为了在自己的程序中有效地运用多态乃至面向对象的技术 必须扩展自己的编程视野 使其不仅包括个别类的成员和消息 而且还要包括类与类之间的共同特性以及它们之间的关系 尽管这需要极大的努力 但是这样做是非常值得的 因为它可以带来很多成效:更快的程序开发过程 更好的代码组织 更好扩展的程序以及更容易的代码维护等