在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时,都可以“生长”的程序。
动态绑定|后期绑定|运行时绑定
向上转型
把某个对象的引用视为其基类型的引用的做法被称为向上转型
——因为在继承树的画法中,基类是放置在上方的。
绑定
方法调用绑定
前期绑定
将一个方法调用同一个方法主体关联起来被称作绑定
。若在程序执行前进行绑定(如果由的话,由编译器和连接程序实现),叫做前期绑定
-
后期绑定
在运行时根据对象的类型进行绑定(后期绑定也叫动态绑定或运行时绑定)。如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能找到正确的方法体并加以调用。Java中除了
static
和final
方法(private
方法属于final方法)之外,其他所有的方法都是后期绑定。
final
可以防止其他人覆盖该方法,可以有效地“关闭”动态绑定。
构造器和多态
构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。
如果基类构造器未能得到调用,对象可能不会构造完整。
构造器会检查对象是否正确被构造
- 调用基类构造器 (该步骤会不断的反复递归,首先构造该层次结构的根,一直到最底层的导出类)
- 按声明顺序调用成员的初始化方法
- 调用导出类构造器的主体
继承与清理
通过组合和继承创建新类的时候,如果有必要需要清理,那么需要为新类创建专门的清理方法,如dispose()
,并且由于继承的缘故,可能需要在导出类中覆盖dispose()
方法。当覆盖(重写)被继承类dispose()
方法时,必须调用基类版本的dispose()
方法,否则基类的清理动作就不会发生。eg. super.dispose();
==对象销毁的顺序应和初始化顺序相反==
对于字段,销毁的顺序应与其声明顺序相反。
字段的初始化是按照声明的顺序进行的对于基类,应该先对其导出类进行清理,然后才是基类。
导出类的清理可能和会调用基类中的某些方法,需要使得基类中的构件仍起作用,而不是过早销毁。
构造器内部的多态方法的行为
如果在一个构造器的内部调用正在构造的对象的某个动态绑定方法,会发生什么?
在一般的方法内部,动态绑定的调用是运行时才决定的,因此对象无法知道它是属于方法所在的那个类,还是属于那个类的导出类。
如果要调用构造器内部的一个动态绑定方法,就要用到那个方法被覆盖后的定义。但被覆盖的方法在可能会在对象被完全构造之前就会被调用,这会造成一些难以发现的隐藏错误。
从概念上讲,构造器的工作实际上是创建对象。在任何构造器内部,整个对象可能只是部分形成(我们只知道基类对象已经进行初始化)。如果构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。
然而,一个动态绑定的方法调用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化!!!
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);
}
void draw() {System.out.println("RoundGlyph.draw(), radius = "+radius);}
}
public class PolyConstructors {
public static void main(String[] args) {
new RoundGlyph(5);
}
}
初始化顺序
- 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的零
- 按顺序从根向下调用基类构造器,此时调用被覆盖后的
draw()
方法(在调用RoundGlyph构造器之前调用)即对象未构造完整时,由于步骤1,此时radius
的值为0 - 按照声明顺序调用成员的初始化方法
- 调用导出类的构造器主题(此时对象才构造完整)
编写构造器时有一条有效的准则
“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”
在构造器内唯一能安全调用的那些方法是基类中的
final方法
(private方法
也适用,它们自动属于final方法
)。这些方法不能被覆盖,因此也就不会出现上述问题。
用继承进行设计
当我们使用现有的类来建立新类时,应该首先考虑使用组合
,组合不会强制我们的程序设计进入继承的层次结构中。而且组合更加灵活,因为它可以动态选择类型(因此也就选择的行为);相反,继承在编译时就需要知道确切类型。
class Actor {
public void act() {System.out.println("Actor");}
}
class HappyActor extends Actor {
public void act() {System.out.println("HappyActor");}
}
class SadActor extends Actor {
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();
}
}
Stage
对象包含一个对Actor
的引用,而Actor
被初始化为HappyActor
对象。这意味着performPlay()
会产生某种特殊行为(动态绑定)。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor
对象的引用也可以在actor
中被替代,然后由performPlay()
产生的行为也随之改变。这样一来我们在运行期间就获得了动态灵活性
(也称作“状态模式”)。与此相反,我们不能在运行期间决定继承不同的对象,因为它要求在编译期间完全确定下来。
纯继承与扩展
- 纯继承
is-a关系
是一种纯替代
,导出类可以完全代替基类,在使用它们时,完全不需要知道关于子类的任何额外信息:
也就是说,基类可以接受发送给导出类的任何消息,因为二者有着完全相同的接口。我们只需要从导出类向上转型,永远不需要知道正在处理的对象的确切类型。即通过多态处理一切。
-
扩展
is-like-a关系
当目的开始转向,我们就不得不去扩展接口,只有这样才能去解决问题。
导出类就像是一个基类——它有着相同的基本接口,但是它还有具有由额外方法实现的其他特性。在具体实现中,
is-like-a关系
是有用且明智的,但是导出类中接口的扩展部分不能被基类访问,因此,一旦我们向上转型,就不能调用那些新方法:
在这种情况下,如果不进行向上转型,问题也就不会出现。但是通常情况下,我们需要重新查明对象的确切类型,以便能够访问该类型所扩充的方法。
向下转型与运行时类型识别
由于向上转型
(在继承层次中向上移动)会丢失具体的类型信息,所以我们就想,通过向下转型
,即在继承层次中向下移动应该可以获取类型信息。
==向上转型是安全的==,因为基类不会具有大于导出类的接口。因此,我们通过基类接口发送的消息都被保证能被接受。但是==向下转型是不安全的==,我们无法知道一个“几何形状”它确实就是一个“圆”,它可以是一个三角形,正方形或其他一些类型。
因此我们需要一种方法来确保向下转型的正确性,使我们不致于贸然转型到一种错误类型,进而发出该对象无法接受的消息。
在Java语言中,所有转型都会得到检查!所以即使我们只是进行一次普通的加括弧形式的类型转换,在进入运行期时仍然会对其进行检查,以便确保它的确是我们希望的那种类型。如果不是,就会返回一个ClassCastException
异常。这种在运行期间对类型检查的行为称作“运行时类型识别”(RTTI)。下例为说明:
class Useful {
public void f() {System.out.println("base");}
public void g() {System.out.println("base");}
}
class MoreUseful extends Useful {
public void f() {System.out.println("notbase");}
public void g() {System.out.println("notbase");}
public void u() {System.out.println("notbase");}
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();
// Downcast success
((MoreUseful)x[1]).u();
// Exception thrown:ClassCastExpection
((MoreUseful)x[0]).u();
}
}
总结
多态意味着“不同的形式”。
在面向对象程序设计中,我们持有从基类继承而来的相同接口,以及使用该接口的不同形式: 不同版本的动态绑定方法
理解和创建多态需要通过数据抽象和继承。
多态是一种不能单独来看待的特性(eg. switch语句是可以的),相反它只能作为类关系“全景”中的一部分,与其他特性协同工作。