<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
在面向对象的程序设计语言中,多态(polymorphic)是继数据抽象和继承之后的第三种基本特性。
多态通过分离“做什么”和“怎么做”,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建“可扩展的”程序,即无论在项目最初创建时,还是在需要添加新功能时,都可以进行扩充。
“封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过细节“私有化(private)”将接口和实现分离开来。这种类型的组织机制对那些有过程化程序设计背景的人来说,更容易理解。而多态的作用则是消除类型之间的耦合关系。在前一章中,我们已经知道继承允许将对象视为自己本身的类型或它的基类型进行处理。这种能力极为重要,因为它可以使多种类型(从同一基类导出而来的)被视为同一类型进行处理,而同一份代码也就可以毫无差别地运行在这些不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都是从同一基类导出而来的。这种区别是根据方法行为的不同来而表示出来的,虽然这些方法都可以通过同一个基类来调用。
在本章中,通过一些基本简单的例子(这些例子中所有与多态无关的代码都被删掉,只剩下与多态有关的部分)来深入浅出地学习多态(也称作动态绑定dynamic binding、后期绑定late binding或运行时绑定run-time binding)。
向上转型
我们已经知道对象既可以作为它自己本身的类型使用,也可以作为它的基类型使用。而这种将对某个对象的引用视为对其基类型的引用的做法被称作“向上转型(upcasting)”――因为在继承树的画法中,基类是放置在上方的。
但是,这样做也会引起一个的问题,具体看下面这个有关乐器的例子。既然几个例子都要演奏乐符(Note),我们就应该在包中单独创建一个Note类。
- //! Note.java
- public class Note {
- private String noteName;
- private Note(String noteName) { //Note的构造器
- this.noteName = noteName;
- }
- public String toString() { //重载Note的toString方法
- return noteName;
- }
- public static final Note
- MIDDLE_C = new Note("Middle C"),//定义了3个Note类型的引用
- C_SHARP = new Note("C_SHARP"),
- B_FLAT = new Note("B Flat");
- }
在下面的例子中,Wind是一种Instrument,因此可以继承Instrument类。
- //! Wind.java
- public class Wind extends Instrument {
- public void play(Note n) { //重写了Instrument的play()方法
- System.out.println("Wind.play()" + n);
- }
- }
- //! Instrument.java
- public class Instrument {
- public void play(Note n) {
- System.out.println("Instrument.play() " + n);
- }
- }
- //! Music.java
- public class Music {
- public static void tune(Instrument i) {
- i.play(Note.MIDDLE_C); //传入一个静态的Note类型的引用,是通过类名.引用名调用的
- }
- public static void main(String[] args) {
- Wind flute = new Wind();
- tune(flute); //将Wind类型的对象flute上转型为Instrument
- }
- }
忘记对象类型
Music.java这个程序看起来似乎有些奇怪。为什么所有人都应该故意忘记一个对象的类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。但这样会引发的一个重要问题是:如果你那样做,就需要为系统内Instrument的每种类型都编写一个新的tune()方法。假设按照这种推理,现在再加入Stringed(弦乐)和Brass(管乐)这两种Instrument(乐器):
<script type="text/javascript"> </script> <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
- class Stringed extends Instrument {
- public void play(Note n) {
- System.out.println("Stringed.play() " + n);
- }
- }
- class Brass extends Instrument {
- public void play(Note n) {
- System.out.println("Brass.play() " + n);
- }
- }
- public class Music2 {
- public static void tune(Wind i) {
- i.play(Note.MIDDLE_C);
- }
- public static void tune(Stringed i) {
- i.play(Note.MIDDLE_C);
- }
- public static void tune(Brass i) {
- i.play(Note.MIDDLE_C);
- }
- public static void main(String[] args) {
- Wind flute = new Wind();
- Stringed violin = new Stringed();
- Brass frenchHorn = new Brass();
- tune(flute); // No upcasting
- tune(violin);
- tune(frenchHorn);
- }
- } ///:~
这样做行得通,但有一个主要缺点:必须为添加的每一个新Instrument类编写特定类型的方法。这意味着在开始时就需要更多的编程,这也意味着如果以后想添加类似Tune()的新方法,或者添加自Instrument导出的新类,仍需要做大量的工作。此外,如果我们忘记重载某个方法,编译器不会返回任何错误信息,这样关于类型的整个处理过程就变得难以操纵。
如果我们只写这样一个简单方法,它仅接收基类作为参数,而不是那些特殊的导出类。这样做情况会变得更好吗?也就是说,如果我们不管导出类的存在,编写的代码只是与基类打交道,会不会好呢?
这正是多态所允许的。然而,大多数程序员具有面向过程程序设计的背景,对多态的运作方式可能会感到有一点迷惑。
曲解
运行Music.java这个程序后,我们便会发现难点所在。Wind.play( )方法将产生输出结果。这无疑是我们所期望的输出结果,但它看起来似乎又没有什么意义。请观察一下tune()方法:
- public static void tune(Instrument i){
- //...
- i.play(Note.MIDDLE_C);
- }
方法调用绑定
将一个方法调用同一个方法主体关联起来被称作“绑定(binding)”。若在程序执行前进行绑定(如果有的话,由编译器和链接程序实现),叫做“前期绑定(early binding)”。可能你以前从来没有听说过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。C编译器只有一种方法调用,那就是前期绑定。
上述程序之所以令人迷惑,主要是因为提前绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。
解决的办法叫做“后期绑定(late binding)”,它的含义就是在运行时,根据对象的类型进行绑定。后期绑定也叫做“动态绑定(dynamic binding)”或“运行时绑定(run-time binding)”。如果一种语言想实现后期绑定,就必须具有某些机制,以便在运行时能判断对象的类型,以调用恰当的方法。也就是说,编译器仍不知道对象的类型,但是方法调用机制能找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是我们只要想象一下就会得知,不管怎样都必须在对象中安置某种“类型信息”。
Java中除了static和final方法(private方法属于final)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定---它会自动发生。
为什么要将某个方法声明为final呢?正如前一章提到的那样,它可以防止其他人重载该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者是想告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对我们程序的整体性能不会产生什么改观。所以,最好根据设计来决定是否使用final ,而不是出于试图提高性能。