在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。
多态通过分离做什么和做怎么做,从另一个将接口和实现分离。
8.1 再论向上转型
对象即可以作为它自己本身的类型使用,也可以作为它的基类使用。这种把对某个对象的引用视为对基类型引用前的做法被称作向上转型。这样做有个问题。
例:
public enum Note{
MIDDLE_C,C_SHARP,B_FLAT;
}
class instrument{
public void play(Note n){
print("instrument.play()");
}
}
public class Wind extends Instrument{
public void play(Note n){
System.out.println("Wind.play()"+n);
}
}
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);
}
}
Music.tune()接收一个Instrument引用。在main方法中tune(flute)传递一个Wind引用,Wind从Instrument继承类,instrument接口一定存在于Wind中,从Wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。
- 8.11忘记对象类型
如果让tune()方法直接接受一个Wind引用作为参数,程序内的Instrument的每种方法都编写一个新的tune()方法。
class Stringed extends Instrument{
public void play(Note n){
print("Stringed.play"+n);
}
}
class Brass extends Instrument{
public void paly(Note n){
print("Brass.paly"+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 fllute = new Wind();
Stringed violin = new Stringed();
Brass frenchHorn = new Brass();
tune(fllute );
tune(violin );
tune(frenchHorn);
}}
这样做工作量很大
如果我们只写这样一个简单的方法,它仅接收基类作为参数,而不是特殊的导出类。也就是说编写代码只与基类打交道,会不会更好???
8.2 转机
public static void tune(Instrument){i.play(Note.MIDDLE_C);}
怎样知道Instrument引用指向的是Wind对象,而不是Brass或Stringed?实际上编译器无法得知。
- 8.21 方法绑定
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定(若果没有的话,由编译器和连接程序实现),叫做前期绑定 编译器一直不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。
上述程序之所以让人迷惑,主要是因为前期绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用的是哪个方法。
解决的办法就是后期绑定,它的含义就是在运行时根据队形的类型进行绑定。(也称作动态绑定)
Java中除Static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
为什么要将某个方法声明为final呢?防止其他人覆盖该方法,更重要的是:有效关闭动态绑定,告诉编译器不需要进行动态绑定。
- 8.22 产生正确行为
一旦知道Java中所有的方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码,并这些代码多有的导出类都可以正确执行。(发送消息给某一个对象,让该对象判断应该做什么)。 - 8.23 可扩展性
由于有多态机制,我们可以根据自己的需求对程序添加任意多的新类型,而不需要修改tune()方法,大多数方法都会遵循tune()模型,而且只与基类接口通讯。这样的程序是可扩展的。
多态是一项让程序员“将改变的事物和未改变的事物分离开”的重要技术 - 8.24 缺陷:“覆盖”私有方法
public class PrivateOverride{
private void f(){ print("private f()"); }
public static void main(String[] args){
PrivateOverride po = new Derived();
po.f();
}
}
class Dervied extends PrivateOverride{
public void f() { print("public f()") }
}
// log---private f()
我们所期望的是输出public f(),由于private方法被自动认为是final方法,对导出类是屏蔽的,因此基类中的f()方法在子类中Dervied中不可见,因此也不能被重载。
结论:只有非private方法才可以被覆盖。在导出类中对于基类中的private方法,采用不同的名字。
- 8.25 缺陷:域与静态方法
class Super{
public int field = 0;
public int getField(){return field;}
}
class Sub entends Super{
public int field = 1;
public int getField(){return field;}
public int getSuperField(){return super.field;}
}
public class Fieldaccess{
public static void main(String[] args){
Super sup = new Sub();
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());
}
}
//log
//sup.field = 0, sup.getField() = 1
//sub.field = 1, sub.getField() =1, sub.getSuperField() =0
Super.field 和 Sub.field分配不同的存储空间,Sub包含两个称为field的域,在引用Sub中的field时所产生的默认域并非Super版本的field域,为了得到Super.getField,必须显示指名super.field,但在实践中,通常会将所有的域都设置成private,就不能直接访问它,只能调用方法来访问。
8.3 构造器的多态
- 8.31 构造器的调用顺序
基类的构造器总是在导出类的构造器中被调用,按照继承层次逐渐向上链接,已使每个基类构造器得到调用。 - 8.32 继承与清理
通过组合和继承方法来创建新类时,永远不用担心对象清理的问题,子对象通常hi留给垃圾回收器来处理。如果遇到清理问题,必须用心为类新建dispose()方法(在这里选用此名)并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类dispose()方法时,务必基础调用基类版本dispose()方法,否则,基类的清理动作不会发生。 - 8.33 构造器内部的多态方法的行为
class Glyph{
void draw(){print("Glyph.draw()");}
Glyph(){
print("Glyph() before draw()";)
draw();
print("Glyph() after draw()");
}
}
class RoundGlyph extends Glyph{
private int radius = 1;
RoundGlyph(int r){
radius = r;
print("RoundGlyph.RoundGlyph(), radius = " + radius);
}
void draw(){
print("RoundGlyph.draw(),radius = " + radius);
}
}
public class PolyConstructors{
public static void main(String[] args){
new RoundGlyph(5);
}
}
//output
//Glyph() before draw()
//RoundGlyph.RoundGlyph(), radius = 0
//Glyph() after draw()
//RoundGlyph.RoundGlyph(), radius = 5
初始化实际过程是
- 1 在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制零。
- 2 调用基类构造器
- 3 按照声明的顺序调用成员的初始化方法
- 4 调用导出类的构造器主体
编写构造器有一条有效的准则;“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法”。在构造器内部唯一能够安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)这些方法不能被覆盖,因此不回出现这样的问题。
8.4 协变返回类型
在导出类中的被覆盖方法可以返回基类方法的返回类型的某种导出类型。
8.5 用继承进行设计
当我们使用现成的类创建新类时,首先选择是“组合”,尤其是不知道使用哪一种方式时,组合不会强制我们的程序设计进入继承的层次结构中,而且更加灵活,可以动态选择类型。
class Actor{
public void act(){}
}
class HappyActor extendsActor{
public void act(){print("HappyActor")}
}
class SadActor extends Actor{
public void act(){ print("SadActor")}
}
class Stage {
private Actor actor = newHappyActor();
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();
}
}
//Output
//HappyActor
//SadActor
一条通用的准则是:用继承表达行为间的差异,并用字段表达状态上的变化,这个例子中两者都用到了:通过继承得到不同的类,用于表达act()方法的差异;而Stage通过运用组合是自己的状态发生变化。这种状态的改变也就产生了行为的改变。