09-Polymorphism
第九章 多态
曾经有人请教我 “
Babbage 先生,如果输入错误的数字到机器中,会得出正确结果吗? ” 我无法理解产生如此问题的概念上的困惑。 —— Charles Babbage (1791 - 1871)
多态是面向对象编程语言中,继数据抽象和继承之外的第三个重要特性。
多态提供了另一个维度的接口与实现分离,以解耦做什么和怎么做。多态不仅能改善代码的组织,提高代码的可读性,而且能创建有扩展性的程序——无论在最初创建项目时还是在添加新特性时都可以“生长”的程序。
封装通过合并特征和行为来创建新的数据类型。隐藏实现通过将细节私有化把接口与实现分离。这种类型的组织机制对于有面向过程编程背景的人来说,更容易理解。而多态是消除类型之间的耦合。在上一章中,继承允许把一个对象视为它本身的类型或它的基类类型。这样就能把很多派生自一个基类的类型当作同一类型处理,因而一段代码就可以无差别地运行在所有不同的类型上了。多态方法调用允许一种类型表现出与相似类型的区别,只要这些类型派生自一个基类。这种区别是当你通过基类调用时,由方法的不同行为表现出来的。
在本章中,通过一些基本、简单的例子(这些例子中只保留程序中与多态有关的行为
向上转型回顾
在上一章中,你看到了如何把一个对象视作它的自身类型或它的基类类型。这种把一个对象引用当作它的基类引用的做法称为向上转型,因为继承图中基类一般都位于最上方。
同样你也在下面的音乐乐器例子中发现了问题。即然几个例子都要演奏乐符(Note
// polymorphism/music/Note.java
// Notes to play on musical instruments
package polymorphism.music;
public enum Note {
MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}
枚举已经在”第
这里,
// polymorphism/music/Instrument.java
package polymorphism.music;
class Instrument {
public void play(Note n) {
System.out.println("Instrument.play()");
}
}
// polymorphism/music/Wind.java
package polymorphism.music;
// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
// Redefine interface method:
@Override
public void play(Note n) {
System.out.println("Wind.play() " + n);
}
}
tune()
接受一个
// polymorphism/music/Music.java
// Inheritance & upcasting
// {java polymorphism.music.Music}
package polymorphism.music;
public class Music {
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void main(String[] args) {
Wind flute = new Wind();
tune(flute); // Upcasting
}
}
输出:
Wind.play() MIDDLE_C
在 main()
中你看到了 tune()
方法传入了一个
忘掉对象类型
tune()
接受的参数是一个tune()
方法。假设按照这种推理,再增加
// polymorphism/music/Music2.java
// Overloading instead of upcasting
// {java polymorphism.music.Music2}
package polymorphism.music;
class Stringed extends Instrument {
@Override
public void play(Note n) {
System.out.println("Stringed.play() " + n);
}
}
class Brass extends Instrument {
@Override
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);
}
}
输出:
Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
这样行得通,但是有一个主要缺点:必须为添加的每个新tune()
的新方法或
如果只写一个方法以基类作为参数,而不用管是哪个具体派生类,这样会变得更好吗?也就是说,如果忘掉派生类,编写的代码只与基类打交道,会不会更好呢?
这正是多态所允许的。但是大部分拥有面向过程编程背景的程序员会对多态的运作方式感到一些困惑。
转机
运行程序后会看到tune()
方法:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
它接受一个
方法调用绑定
将一个方法调用和一个方法主体关联起来称作绑定。若绑定发生在程序运行前(如果有的话,由编译器和链接器实现
上述程序让人困惑的地方就在于前期绑定,因为编译器只知道一个
解决方法就是后期绑定,意味着在运行时根据对象的类型进行绑定。后期绑定也称为动态绑定或运行时绑定。当一种语言实现了后期绑定,就必须具有某种机制在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器仍然不知道对象的类型,但是方法调用机制能找到正确的方法体并调用。每种语言的后期绑定机制都不同,但是可以想到,对象中一定存在某种类型信息。
为什么将一个对象指明为
产生正确的行为
一旦当你知道
面向对象编程中的经典例子是形状
形状的例子中,有一个基类称为
向上转型就像下面这么简单:
Shape s = new Circle();
这会创建一个
假设你调用了一个基类方法(在各个派生类中都被重写
s.draw()
你可能再次认为draw()
方法被调用,因为draw()
方法,这是正确的。
下面的例子稍微有些不同。首先让我们创建一个可复用的
// polymorphism/shape/Shape.java
package polymorphism.shape;
public class Shape {
public void draw() {}
public void erase() {}
}
派生类通过重写这些方法为每个具体的形状提供独一无二的方法行为:
// polymorphism/shape/Circle.java
package polymorphism.shape;
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Circle.draw()");
}
@Override
public void erase() {
System.out.println("Circle.erase()");
}
}
// polymorphism/shape/Square.java
package polymorphism.shape;
public class Square extends Shape {
@Override
public void draw() {
System.out.println("Square.draw()");
}
@Override
public void erase() {
System.out.println("Square.erase()");
}
}
// polymorphism/shape/Triangle.java
package polymorphism.shape;
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("Triangle.draw()");
}
@Override
public void erase() {
System.out.println("Triangle.erase()");
}
}
get()
方法时,就会产生一个指向随机创建的get()
方法发送出去。因此无论何时调用 get()
方法,你都无法知道具体的类型是什么,因为你总是得到一个简单的
// polymorphism/shape/RandomShapes.java
// A "factory" that randomly creates shapes
package polymorphism.shape;
import java.util.*;
public class RandomShapes {
private Random rand = new Random(47);
public Shape get() {
switch(rand.nextInt(3)) {
default:
case 0: return new Circle();
case 1: return new Square();
case 2: return new Triangle();
}
}
public Shape[] array(int sz) {
Shape[] shapes = new Shape[sz];
// Fill up the array with shapes:
for (int i = 0; i < shapes.length; i++) {
shapes[i] = get();
}
return shapes;
}
}
array()
方法分配并填充了
// polymorphism/Shapes.java
// Polymorphism in Java
import polymorphism.shape.*;
public class Shapes {
public static void main(String[] args) {
RandomShapes gen = new RandomShapes();
// Make polymorphic method calls:
for (Shape shape: gen.array(9)) {
shape.draw();
}
}
}
输出:
Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
main()
方法中包含了一个get()
方法生成。现在你只知道拥有一些形状,但除此之外一无所知(编译器也是如此draw()
方法时,从运行程序的结果中可以看到,与类型有关的特定行为奇迹般地发生了。
随机生成形状是为了让大家理解:在编译时,编译器不需要知道任何具体信息以进行正确的调用。所有对方法 draw()
的调用都是通过动态绑定进行的。
可扩展性
现在让我们回头看音乐乐器的例子。由于多态机制,你可以向系统中添加任意多的新类型,而不需要修改 tune()
方法。在一个设计良好的面向对象程序中,许多方法将会遵循 tune()
的模型,只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类派生出新的数据类型,从而添加新的功能。那些操纵基类接口的方法不需要改动就可以应用于新类。
考虑一下乐器的例子,如果在基类中添加更多的方法,并加入一些新类,将会发生什么呢:
所有的新类都可以和原有类正常运行,不需要改动 tune()
方法。即使 tune()
方法单独存放在某个文件中,而且向tune()
方法也无需再编译就能正确运行。下面是类图的实现:
// polymorphism/music3/Music3.java
// An extensible program
// {java polymorphism.music3.Music3}
package polymorphism.music3;
import polymorphism.music.Note;
class Instrument {
void play(Note n) {
System.out.println("Instrument.play() " + n);
}
String what() {
return "Instrument";
}
void adjust() {
System.out.println("Adjusting Instrument");
}
}
class Wind extends Instrument {
@Override
void play(Note n) {
System.out.println("Wind.play() " + n);
}
@Override
String what() {
return "Wind";
}
@Override
void adjust() {
System.out.println("Adjusting Wind");
}
}
class Percussion extends Instrument {
@Override
void play(Note n) {
System.out.println("Percussion.play() " + n);
}
@Override
String what() {
return "Percussion";
}
@Override
void adjust() {
System.out.println("Adjusting Percussion");
}
}
class Stringed extends Instrument {
@Override
void play(Note n) {
System.out.println("Stringed.play() " + n);
}
@Override
String what() {
return "Stringed";
}
@Override
void adjust() {
System.out.println("Adjusting Stringed");
}
}
class Brass extends Wind {
@Override
void play(Note n) {
System.out.println("Brass.play() " + n);
}
@Override
void adjust() {
System.out.println("Adjusting Brass");
}
}
class Woodwind extends Wind {
@Override
void play(Note n) {
System.out.println("Woodwind.play() " + n);
}
@Override
String what() {
return "Woodwind";
}
}
public class Music3 {
// Doesn't care about type, so new types
// added to the system still work right:
public static void tune(Instrument i) {
// ...
i.play(Note.MIDDLE_C);
}
public static void tuneAll(Instrument[] e) {
for (Instrument i: e) {
tune(i);
}
}
public static void main(String[] args) {
// Upcasting during addition to the array:
Instrument[] orchestra = {
new Wind(),
new Percussion(),
new Stringed(),
new Brass(),
new Woodwind()
};
tuneAll(orchestra);
}
}
输出:
Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C
新方法 what()
返回一个带有类描述的adjust()
提供一些乐器调音的方法。
在 main()
方法中,当向
tune()
方法可以忽略周围所有代码发生的变化,仍然可以正常运行。这正是我们期待多态能提供的特性。代码中的修改不会破坏程序中其他不应受到影响的部分。换句话说,多态是一项“将改变的事物与不变的事物分离”的重要技术。
陷阱: “重写”私有方法
你可能天真地试图像下面这样做:
// polymorphism/PrivateOverride.java
// Trying to override a private method
// {java polymorphism.PrivateOverride}
package polymorphism;
public class PrivateOverride {
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride po = new Derived();
po.f();
}
}
class Derived extends PrivateOverride {
public void f() {
System.out.println("public f()");
}
}
输出:
private f()
你可能期望输出是f()
是一个全新的方法;因为基类版本的 f()
屏蔽了
结论是只有非
如果使用了 @Override
注解,就能检测出问题:
// polymorphism/PrivateOverride2.java
// Detecting a mistaken override using @Override
// {WillNotCompile}
package polymorphism;
public class PrivateOverride2 {
private void f() {
System.out.println("private f()");
}
public static void main(String[] args) {
PrivateOverride2 po = new Derived2();
po.f();
}
}
class Derived2 extends PrivateOverride2 {
@Override
public void f() {
System.out.println("public f()");
}
}
编译器报错信息是:
error: method does not override or
implement a method from a supertype
陷阱:属性与静态方法
一旦学会了多态,就可以以多态的思维方式考虑每件事。然而,只有普通的方法调用可以是多态的。例如,如果你直接访问一个属性,该访问会在编译时解析:
// polymorphism/FieldAccess.java
// Direct field access is determined at compile time
class Super {
public int field = 0;
public int getField() {
return field;
}
}
class Sub extends Super {
public int field = 1;
@Override
public int getField() {
return field;
}
public int getSuperField() {
return super.field;
}
}
public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
System.out.println("sup.field = " + sup.field +
", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " + sub.field +
", sub.getField() = " + sub.getField()
+ ", sub.getSuperField() = " + sub.getSuperField())
}
}
输出:
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
当
尽管这看起来是个令人困惑的问题,实际上基本不会发生。首先,通常会将所有的属性都指明为
如果一个方法是静态
// polymorphism/StaticPolymorphism.java
// static methods are not polymorphic
class StaticSuper {
public static String staticGet() {
return "Base staticGet()";
}
public String dynamicGet() {
return "Base dynamicGet()";
}
}
class StaticSub extends StaticSuper {
public static String staticGet() {
return "Derived staticGet()";
}
@Override
public String dynamicGet() {
return "Derived dynamicGet()";
}
}
public class StaticPolymorphism {
public static void main(String[] args) {
StaticSuper sup = new StaticSub(); // Upcast
System.out.println(StaticSuper.staticGet());
System.out.println(sup.dynamicGet());
}
}
输出:
Base staticGet()
Derived dynamicGet()
静态的方法只与类关联,与单个的对象无关。
构造器和多态
通常,构造器不同于其他类型的方法。在涉及多态时也是如此。尽管构造器不具有多态性(事实上人们会把它看作是隐式声明的静态方法
构造器调用顺序
在“初始化和清理”和“复用”两章中已经简单地介绍过构造器的调用顺序,但那时还没有介绍多态。
在派生类的构造过程中总会调用基类的构造器。初始化会自动按继承层次结构上移,因此每个基类的构造器都会被调用到。这么做是有意义的,因为构造器有着特殊的任务:检查对象是否被正确地构造。由于属性通常声明为
下面的例子展示了组合、继承和多态在构建顺序上的作用:
// polymorphism/Sandwich.java
// Order of constructor calls
// {java polymorphism.Sandwich}
package polymorphism;
class Meal {
Meal() {
System.out.println("Meal()");
}
}
class Bread {
Bread() {
System.out.println("Bread()");
}
}
class Cheese {
Cheese() {
System.out.println("Cheese()");
}
}
class Lettuce {
Lettuce() {
System.out.println("Lettuce()");
}
}
class Lunch extends Meal {
Lunch() {
System.out.println("Lunch()");
}
}
class PortableLunch extends Lunch {
PortableLunch() {
System.out.println("PortableLunch()");
}
}
public class Sandwich extends PortableLunch {
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich() {
System.out.println("Sandwich()");
}
public static void main(String[] args) {
new Sandwich();
}
}
输出:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
这个例子用其他类创建了一个复杂的类。每个类都在构造器中声明自己。重要的类是
从创建
- 基类构造器被调用。这个步骤被递归地重复,这样一来类层次的顶级父类会被最先构造,然后是它的派生类,以此类推,直到最底层的派生类。
- 按声明顺序初始化成员。
- 调用派生类构造器的方法体。
构造器的调用顺序很重要。当使用继承时,就已经知道了基类的一切,并可以访问基类中任意
在构造器中必须确保所有的成员都已经构建完。唯一能保证这点的方法就是首先调用基类的构造器。接着,在派生类的构造器中,所有你可以访问的基类成员都已经初始化。另一个在构造器中能知道所有成员都是有效的理由是:无论何时有可能的话,你应该在所有成员对象(通过组合将对象置于类中)定义处初始化它们(例如,例子中的
不幸的是,这不能处理所有情况,在下一节会看到。
继承和清理
在使用组合和继承创建新类时,大部分时候你无需关心清理。子对象通常会留给垃圾收集器处理。如果你存在清理问题,那么必须用心地为新类创建一个 dispose()
方法(这里用的是我选择的名称,你可以使用更好的名称dispose()
方法。当重写 dispose()
方法时,记得调用基类的 dispose()
方法,否则基类的清理工作不会发生:
// polymorphism/Frog.java
// Cleanup and inheritance
// {java polymorphism.Frog}
package polymorphism;
class Characteristic {
private String s;
Characteristic(String s) {
this.s = s;
System.out.println("Creating Characteristic " + s);
}
protected void dispose() {
System.out.println("disposing Characteristic " + s);
}
}
class Description {
private String s;
Description(String s) {
this.s = s;
System.out.println("Creating Description " + s);
}
protected void dispose() {
System.out.println("disposing Description " + s);
}
}
class LivingCreature {
private Characteristic p = new Characteristic("is alive");
private Description t = new Description("Basic Living Creature");
LivingCreature() {
System.out.println("LivingCreature()");
}
protected void dispose() {
System.out.println("LivingCreature dispose");
t.dispose();
p.dispose();
}
}
class Animal extends LivingCreature {
private Characteristic p = new Characteristic("has heart");
private Description t = new Description("Animal not Vegetable");
Animal() {
System.out.println("Animal()");
}
@Override
protected void dispose() {
System.out.println("Animal dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
class Amphibian extends Animal {
private Characteristic p = new Characteristic("can live in water");
private Description t = new Description("Both water and land");
Amphibian() {
System.out.println("Amphibian()");
}
@Override
protected void dispose() {
System.out.println("Amphibian dispose");
t.dispose();
p.dispose();
super.dispose();
}
}
public class Frog extends Amphibian {
private Characteristic p = new Characteristic("Croaks");
private Description t = new Description("Eats Bugs");
public Frog() {
System.out.println("Frog()");
}
@Override
protected void dispose() {
System.out.println("Frog dispose");
t.dispose();
p.dispose();
super.dispose();
}
public static void main(String[] args) {
Frog frog = new Frog();
System.out.println("Bye!");
frog.dispose();
}
}
输出:
Creating Characteristic is alive
Creating Description Basic Living Creature
LivingCreature()
Creating Characteristiv has heart
Creating Description Animal not Vegetable
Animal()
Creating Characteristic can live in water
Creating Description Both water and land
Amphibian()
Creating Characteristic Croaks
Creating Description Eats Bugs
Frog()
Bye!
Frog dispose
disposing Description Eats Bugs
disposing Characteristic Croaks
Amphibian dispose
disposing Description Both wanter and land
disposing Characteristic can live in water
Animal dispose
disposing Description Animal not Vegetable
disposing Characteristic has heart
LivingCreature dispose
disposing Description Basic Living Creature
disposing Characteristic is alive
层级结构中的每个类都有
尽管通常不必进行清理工作,但万一需要时,就得谨慎小心地执行。
dispose()
方法。然而,一旦某个成员对象被其它一个或多个对象共享时,问题就变得复杂了,不能只是简单地调用 dispose()
。这里,也许就必须使用引用计数来跟踪仍然访问着共享对象的对象数量,如下:
// polymorphism/ReferenceCounting.java
// Cleaning up shared member objects
class Shared {
private int refcount = 0;
private static long counter = 0;
private final long id = counter++;
Shared() {
System.out.println("Creating " + this);
}
public void addRef() {
refcount++;
}
protected void dispose() {
if (--refcount == 0) {
System.out.println("Disposing " + this);
}
}
@Override
public String toString() {
return "Shared " + id;
}
}
class Composing {
private Shared shared;
private static long counter = 0;
private final long id = counter++;
Composing(Shared shared) {
System.out.println("Creating " + this);
this.shared = shared;
this.shared.addRef();
}
protected void dispose() {
System.out.println("disposing " + this);
shared.dispose();
}
@Override
public String toString() {
return "Composing " + id;
}
}
public class ReferenceCounting {
public static void main(String[] args) {
Shared shared = new Shared();
Composing[] composing = {
new Composing(shared),
new Composing(shared),
new Composing(shared),
new Composing(shared),
new Composing(shared),
};
for (Composing c: composing) {
c.dispose();
}
}
}
输出:
Creating Shared 0
Creating Composing 0
Creating Composing 1
Creating Composing 2
Creating Composing 3
Creating Composing 4
disposing Composing 0
disposing Composing 1
disposing Composing 2
disposing Composing 3
disposing Composing 4
Disposing Shared 0
在将一个addRef()
,而 dispose()
方法会跟踪引用数,以确定在何时真正地执行清理工作。使用这种技巧需要加倍细心,但是如果需要清理正在共享的对象,你没有太多选择。
构造器内部多态方法的行为
构造器调用的层次结构带来了一个困境。如果在构造器中调用了正在构造的对象的动态绑定方法,会发生什么呢?
在普通的方法中,动态绑定的调用是在运行时解析的,因为对象不知道它属于方法所在的类还是类的派生类。
如果在构造器中调用了动态绑定方法,就会用到那个方法的重写定义。然而,调用的结果难以预料因为被重写的方法在对象被完全构造出来之前已经被调用,这使得一些
从概念上讲,构造器的工作就是创建对象(这并非是平常的工作
下面例子展示了这个问题:
// polymorphism/PolyConstructors.java
// Constructors and polymorphism
// don't produce what you might expect
class Glyph {
void draw() {
System.out.println("Glyph.draw()");
}
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph {
private int radius = 1;
RoundGlyph(int r) {
radius = r;
System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
}
@Override
void draw() {
System.out.println("RoundGlyph.draw(), radius = " + radius);
}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
输出:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
draw()
被设计为可重写,在draw()
方法,这看起来正是我们的目的。输出结果表明,当draw()
时,
前一小节描述的初始化顺序并不十分完整,而这正是解决谜团的关键所在。初始化的实际过程是:
- 在所有事发生前,分配给对象的存储空间会被初始化为二进制
0 。 - 如前所述调用基类构造器。此时调用重写后的
draw()
方法(是的,在调用RoundGraph 构造器之前调用 ) ,由步骤1 可知,radius 的值为 0 。 - 按声明顺序初始化成员。
- 最终调用派生类的构造器。
这么做有个优点:所有事物至少初始化为
另一方面,应该震惊于输出结果。逻辑方面我们已经做得非常完美,然而行为仍不可思议的错了,编译器也没有报错(
因此,编写构造器有一条良好规范:做尽量少的事让对象进入良好状态。如果有可能的话,尽量不要调用类中的任何方法。在基类的构造器中能安全调用的只有基类的
协变返回类型
// polymorphism/CovariantReturn.java
class Grain {
@Override
public String toString() {
return "Grain";
}
}
class Wheat extends Grain {
@Override
public String toString() {
return "Wheat";
}
}
class Mill {
Grain process() {
return new Grain();
}
}
class WheatMill extends Mill {
@Override
Wheat process() {
return new Wheat();
}
}
public class CovariantReturn {
public static void main(String[] args) {
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m = new WheatMill();
g = m.process();
System.out.println(g);
}
}
输出:
Grain
Wheat
关键区别在于process()
方法必须返回
使用继承设计
学习过多态之后,一切看似都可以被继承,因为多态是如此巧妙的工具。这会给设计带来负担。事实上,如果利用已有类创建新类首先选择继承的话,事情会变得莫名的复杂。
更好的方法是首先选择组合,特别是不知道该使用哪种方法时。组合不会强制设计是继承层次结构,而且组合更加灵活,因为可以动态地选择类型(因而选择相应的行为
// 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
performPlay()
会产生一个特殊行为。但是既然引用可以在运行时与其他不同的对象绑定,那么它就可以被替换成对performPlay()
的行为随之改变。这样你就获得了运行时的动态灵活性(这被称为状态模式
有一条通用准则:使用继承表达行为的差异,使用属性表达状态的变化。在上个例子中,两者都用到了。通过继承得到的两个不同类在 act()
方法中表达了不同的行为,
替代vs 扩展
采用“纯粹”的方式创建继承层次结构看上去是最清晰的方法。即只有基类的方法才能在派生类中被重写,就像下图这样:
这被称作纯粹的“
纯粹的替代意味着派生类可以完美地替代基类,当使用它们时,完全不需要知道这些子类的信息。也就是说,基类可以接收任意发送给派生类的消息,因为它们具有完全相同的接口。只需将派生类向上转型,不要关注对象的具体类型。所有一切都可以通过多态处理。
按这种方式思考,似乎只有纯粹的“is - a”关系才是唯一明智的做法,其他任何设计只会导致混乱且注定失败。这其实也是个陷阱。一旦按这种方式开始思考,就会转而发现继承扩展接口(遗憾的是,
虽然这是一种有用且明智的方法(依赖具体情况
如果不向上转型,就不会遇到这个问题。但是通常情况下,我们需要重新查明对象的确切类型,从而能够访问该类型中的扩展方法。下一节说明如何做到这点。
向下转型与运行时类型信息
由于向上转型(在继承层次中向上移动)会丢失具体的类型信息,那么为了重新获取类型信息,就需要在继承层次中向下移动,使用向下转型。
向上转型永远是安全的,因为基类不会具有比派生类更多的接口。因此,每条发送给基类接口的消息都能被接收。但是对于向下转型,你无法知道一个形状是圆,它有可能是三角形、正方形或其他一些类型。
为了解决这个问题,必须得有某种方法确保向下转型是正确的,防止意外转型到一个错误类型,进而发送对象无法接收的消息。这么做是不安全的。
在某些语言中(如
// polymorphism/RTTI.java
// Downcasting & Runtime type information (RTTI)
// {ThrowsException}
class Useful {
public void f() {}
public void g() {}
}
class MoreUseful extends Useful {
@Override
public void f() {}
@Override
public void g() {}
public void u() {}
public void v() {}
public void w() {}
}
public class RTTI {
public static void main(String[] args) {
Useful[] x = {
new Useful(),
new MoreUseful()
};
x[0].f();
x[1].g();
// Compile time: method not found in Useful:
//- x[1].u();
((MoreUseful) x[1]).u(); // Downcast/RTTI
((MoreUseful) x[0]).u(); // Exception thrown
}
}
输出:
Exception in thread "main"
java.lang.ClassCastException: Useful cannot be cast to
MoreUseful
at RTTI.main
正如前面类图所示,main()
方法中可以看到这种情况的发生。因为两个对象都是f()
和 g()
方法。如果试图调用 u()
方法(只存在于
为了访问
本章小结
多态意味着“不同的形式”。在面向对象编程中,我们持有从基类继承而来的相同接口和使用该接口的不同形式:不同版本的动态绑定方法。
在本章中,你可以看到,如果不使用数据抽象和继承,就不可能理解甚至创建多态的例子。多态是一种不能单独看待的特性(比如像
为了在程序中有效地使用多态乃至面向对象的技术,就必须扩展自己的编程视野,不能只看到单一类中的成员和消息,而要看到类之间的共同特性和它们之间的关系。尽管这需要很大的努力,但是这么做是值得的。它能带来更快的程序开发、更好的代码组织、扩展性更好的程序和更易维护的代码。
但是记住,多态可能被滥用。仔细分析代码以确保多态确实能带来好处。