对于面向对象的程序设计语言,多型性是第三种最基本的特征前两种是数据抽象和继承。

“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长

1.上溯造型

将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得

一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。

package c07;
class Note 
{
  private int value;
  private Note(int val) { value = val; }
  public static final Note
  middleC = new Note(0),
  cSharp = new Note(1),
  cFlat = new Note(2);
}
class Instrument 
{
  public void play(Note n) {
  System.out.println("Instrument.play()");
  }
}
// Wind objects are instruments
// because they have the same interface:
class Wind extends Instrument
 {
  // Redefine interface method:
  public void play(Note n) {
  System.out.println("Wind.play()");
  }
}
public class Music
 {
  public static void tune(Instrument i) {
  // ...
  i.play(Note.middleC);
  }
  public static void main(String[] args) {
  Wind flute = new Wind();
  tune(flute); // Upcasting
  }
}

其中,方法 Music.tune()接收一个Instrument 句柄,同时也接收从Instrument 衍生出来的所有东西。当一个Wind 句柄传递给 tune()的时候,就会出现这种情况。

为什么要上溯造型

这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument 的每种类型写一个全新的tune()

假设按照前面的推论,加入 Stringed(弦乐)和 Brass(铜管)这两种Instrument(乐器):

......
public class Music2 
{
  public static void tune(Wind2 i) {
  i.play(Note2.middleC);
  }
  public static void tune(Stringed2 i) {
  i.play(Note2.middleC);
  }
  public static void tune(Brass2 i) {
  i.play(Note2.middleC);
  }
  public static void main(String[] args) {
  Wind2 flute = new Wind2();
  Stringed2 violin = new Stringed2();
  Brass2 frenchHorn = new Brass2();
  tune(flute); // No upcasting
  tune(violin);
  tune(frenchHorn);
  }
}

这样写会增加大量代码,类型的整个操作过程就显得极难管理,有失控的危险。

2.深入理解

对于Music.java 的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。

它接收Instrument 句柄。所以在这种情况下,编译器怎样才能知道 Instrument句柄指向的是一个 Wind,而不是一个Brass Stringed 呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。

2.1方法调用的绑定

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。

解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。

Java 中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

为什么要把一个方法声明成final 呢?正如final的使用中指出的那样,它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final 方法调用生成效率更高的代码。

2.2产生正确的行为

在面向对象的程序设计中,有一个经典的“形状”例子。由于它很容易用可视化的形式表现出来,所以经常都用它说明问题。

形状例子有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形),Square(方形),Triangle(三角形)等等。

wKiom1Z35Cagkq9dAACVB97hsAE580.png

上溯造型可用下面这个语句简单地表现出来:

Shape s = new Circle();

2.3扩展性

现在,让我们仍然返回乐器(Instrument)示例。由于存在多形性,所以可根据自己的需要向系统里加入任意多的新类型,同时毋需更改tune()方法。在一个设计良好的 OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。如果是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不需要改变,

对于乐器例子,假设我们在基础类里加入更多的方法,以及一系列新类,下面是示意图:

wKiom1Z35FqQZZprAACUKa2OhdQ653.png

所有这些新类都能与老类——tune()默契地工作,毋需对tune()作任何调整。即使 tune()位于一个独立的文件里,而将新方法添加到Instrument 的接口,tune()也能正确地工作,不需要重新编译。下面这个程序是对上述示意图的具体实现:

import java.util.*;
class Instrument3 
{
  public void play() {
  System.out.println("Instrument3.play()");
  }
  public String what() {
  return "Instrument3";
  }
  public void adjust() {}
 }
class Wind3 extends Instrument3 
{
  public void play() {
  System.out.println("Wind3.play()");
  }
  public String what() { return "Wind3"; }
  public void adjust() {}
}
class Percussion3 extends Instrument3
 {
  public void play() {
  System.out.println("Percussion3.play()");
  }
  public String what() { return "Percussion3"; }
  public void adjust() {}
}
class Stringed3 extends Instrument3
 {
  public void play() {
  System.out.println("Stringed3.play()");
  }
  public String what() { return "Stringed3"; }
  public void adjust() {}
}
class Brass3 extends Wind3
 {
  public void play() {
  System.out.println("Brass3.play()");
  }
  public void adjust() {
  System.out.println("Brass3.adjust()");
  }
}
class Woodwind3 extends Wind3
 {
  public void play() {
  System.out.println("Woodwind3.play()");
  }
  public String what() { return "Woodwind3"; }
}
public class Music3
 {
  // Doesn't care about type, so new types
  // added to the system still work right:
  static void tune(Instrument3 i) {
  // ...
  i.play();
  }
  static void tuneAll(Instrument3[] e) {
  for(int i = 0; i < e.length; i++)
  tune(e[i]);
  }
  public static void main(String[] args) {
  Instrument3[] orchestra = new Instrument3[5];
  int i = 0;
  // Upcasting during addition to the array:
  orchestra[i++] = new Wind3();
  orchestra[i++] = new Percussion3();
  orchestra[i++] = new Stringed3();
  orchestra[i++] = new Brass3();
  orchestra[i++] = new Woodwind3();
  tuneAll(orchestra);
  }
}

新方法是what()adjust()。前者返回一个 String句柄,同时返回对那个类的说明;后者使我们能对每种乐器进行调整。

main()中,当我们将某样东西置入Instrument3数组时,就会自动上溯造型到 Instrument3

可以看到,在围绕tune()方法的其他所有代码都发生变化的同时,tune()方法却丝毫不受它们的影响。

3.覆盖与过载

“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都

只有一种含义,只是原先的含义完全被后来的含义取代了。