在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特性。
1.再论向上转型
public enum Note{
MIDDLE_C,C_SHARP,B_FLAT;
}
class Instrument{
public void play(Note n){
print("Instrument.play()");
}
class Wind extends Instument{
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);
}
}/*Output:
wind.play()MIDDLE_C
从wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。
2.转机
编译器怎么样才能知道这个Instrument引用指向的是wind对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下绑定这个话题。
2.1方法调用绑定
将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前进行绑定,叫做前期绑定。
上述问题的解决方法就是后期绑定,它是在运行时根据对象的类型进行绑定。
Java中除了static和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。
2.2产生正确的行为
动态绑定可以通过正确的方法产生正确的调用。
我们创建Shape s=new Cricle();并且调用s.draw();我们有的是Shape对象,但是为什么没调用Shape的draw()方法呢,由于后期绑定,我们正确的调用了Cricle.draw()方法。
2.3可扩展性
由于多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需更改tune()方法。在一个设计良好的OOP程序中,大多数或者所有方法都会遵循tune()的模型,而且只与基类接口通信。这样的程序是可扩展的,因为可以从通用的基类继承出新的数据类型,从而新添一些功能。那些操纵基类接口的方法不需要任何改动就可以应用于新类。
我们定义Instrument[] arc={new wind(),new Percussion(),new Stringed(),new Woodwind(),new Brass()};所有元素都可以正确的调用自身的play()方法(子类的子类也可以调用自身的方法)。
2.4缺陷:“覆盖”私有方法
<span style="font-size:14px;">public class PrivateOverride{
private void f(){print("print f()");}
public static void main(String[] args){
PrivateOverride po=new Derived();
po.f();
}
}
class Derived extends PrivateOverride{
public void f(){print("public f()");}
}/*Output:
private f()
*///:~</span>
2.5缺陷:域与静态方法
看到这里,认为所有事物都可以多态地发生,这是错误的。只有普通的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问将在编译期进行解析。class Super{
public int field=0;
public int getField(){return field;}
}
class Sub extends Super{
public int field=1;
public int getField(){return field;}
public int getSuperField(){return super.field;}
}
public class access{
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());
}
}/*Output:
sup.field=0,sup.getField()=1
sub.field=1,sub.getField()=1,sub.getSuperField()=0
*///:~
此处,sup.field为0,说明直接访问某个域是没有多态机制的。 静态方法的行为也是不具有多态性的。静态方法是与类,而非与单个对象相关联的。
3.构造器和多态
构造器并不具有多态性(他们实际是static方法,只不过该static声明是隐式的)。但是还是非常有必要理解构造器怎么样通过多态在复杂的结构中运作。3.1构造器的调用顺序
- 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造器这种层次结构的根,然后是下一层导出类,等等,知道最底层的导出类。
- 按声明顺序调用成员的初始化方法。
- 调用那个导出类构造器的主体。
3.2继承与清理
通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果确实遇到清理问题,那么必须用心为新类创建dispose()方法。当覆盖dispose()方法时,务必记住调用基类版本dispose(),否则,基类的清理动作就不会发生。
3.3构造器内部的多态方法的行为
如果要调用构造器内部的一个动态方法,就要用到那个方法被覆盖后的定义。然而,这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就被调用。这可能会造成一些难于发现的隐藏错误。
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(){
radius=r;
print("RoundGlyph.RoundGlyph().radius="+radius);
}
void draw(){
print("RoundGlyph.draw().radius="+radius);
}
}
public class Poly{
public static void main(String[] args){
new RoundGlyph(5);
}
}/*Output:
Glyph() before draw()
RoundGlyph.draw(),radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius=5
*///:~
我们发现当Glyph调用draw()方法时,radius不是默认初始值1,而是0。这说明初始化顺序并不完整。
因此,编写构造器时有一条有效的准则:“用尽可能简单的方法使对象进入正常状态;如果可以的话,避免调用其他方法。”在构造器中唯一能安全调用的方法是基类的final方法(也适用于private方法,因为他们自动属于final方法)。这些方法不能被覆盖,也就不存在上面的问题。
4.用继承进行设计
导出类中接口的扩展部分不能被基类访问,因此,一旦向上转型,我们就不能调用那些新方法。
由于向上转型会丢失具体的类型信息,所以我们就想,通过向下转型,应该能够获取类型信息。但是必须有某种方法来保证向下转型的正确性,使我们不至于贸然转型到一个错误的类型。Java中所有转型都会得到检查。以便保证他的正确性。如果不是,就会返回一个ClassCastException(类转型异常)。这种在运行期间对类型进行检查的行为称为“运行时类型识别”(RTTI)。
class useful{
public void f(){}
public void g(){}
}
class MoreUserful extends useful{
public void f(){}
public void g(){}
public void u(){}
}
public class RTTI{
public static void main(){
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
}
}