多态的概念和一些注意事项

一、问题的提出

由类的继承相关的知识(书第7章)可知,继承允许将对象视为它本身的类型或者其基类型(父类)来处理。这种把某个对象的引用视为对其基类的引用的做法叫做向上转型——在UML图中,基类是放在上方的。来看一个有关乐器的例子:


其中,Instrument(乐器)是基类,3个导出类Wind(管乐器)、Percussion(打击乐器)、Stringed(弦乐器)。乐器需要演奏乐符(Note),先定义一个Note枚举类:

public enum Note {
    MIDDLE_C, C_SHARP, B_FLAT;
}
Instrument类、Wind类、Music类定义如下(只定义了play()方法用于重写):
class Instrument{
    public void play(Note n){
        System.out.println("Instrument.play()");
    }
}
class Wind extends Instrument{
    @Override
    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

Music.tune()方法接受一个Instrument引用,同时也接受任何从Instrument导出的类,而不需要任何类型转换。在这里即用到了 向上转型,使用者可以“忘记”对象的类型,只需要知道tune()方法接受一个Instrument类或其子类对象就行了。如果为每个导出类(例如Percussion、Stringed)都写一个方法,看起来也许更直观,但是也意味着为每一个新导出的Instrument子类都编写一个特定的方法,需要大量的工作。
注意上面的输出结果,i.play()调用的其实是Wind对象的play()方法。那么问题来了,编译器怎么知道这个Instrument引用指向的是Wind对象呢,还是其他诸如Percussion、Stringed对象呢?事实上, 编译器在编译时无法得知
将一个方法的调用同一个方法主题关联起来称作 绑定。若程序执行前进行绑定,叫做 前期绑定(C语言的实现方式)。在上面的例子中,若采用前期绑定的思路,编译器只有一个Instrument引用,是无法得知究竟该调用哪个对象的方法才对。解决的办法是 后期绑定(运行时绑定、动态绑定)要想实现动态绑定,就必须有某种机制,以便在运行时能判断对象的真实类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制可以找到正确的方法体加以调用,因此可以推断需要在对象中安置某种“类型信息”,才能实现动态绑定。


二、Java中的动态绑定和例外情况
Java中除了 static方法final方法(private方法属于final方法)之外,其他所有的方法均为动态绑定。在通常情况下,不需要像C++一样判断何时才需要动态绑定,动态绑定会自动发生。有一些例外的情况不会进行动态绑定,如下:
1. final方法:若某个方法被声明为final,可以防止其他人覆盖该方法。编译器在检查到某个方法为final时,会认为编写者不需要动态绑定,可以为final方法调用生成更有效的代码。
2. 域:如果直接访问类中的某个域,那么这个访问将在编译期间被解析,不会发生动态绑定。
eg.
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();
        System.out.println("sup.field="+sup.field+", sup.getField()="+sup.getField());
        Sub sub=new Sub();
        System.out.println("sub.field="+sub.field+", sup.getField()="+sup.getField()+", sub.getSuperField()="+sub.getSuperField());
    }
}
Output:
sup.field=0, sup.getField()=1
sub.field=1, sup.getField()=1, sub.getSuperField()=0

当Sub对象向上转型为Super引用时,任何域的访问均由编译器解析,也就是前期绑定,因此不是多态的。Super.field和Sub.field其实被分配了两个不同的存储空间,这样一来Sub实际上包含两个称为field的域:它自己的和它从Super处继承的。
3. 静态方法:如果某个方法是静态的,那么它是和类相关联,而不是和某个对象关联,因此不具备多态性。
eg.
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()";
    }
    public String dynamicGet(){
        return "Derived dynamicGet()";
    }
}
 
public class StaticPolymorphism {
 
    public static void main(String[] args) {
        StaticSuper sup=new StaticSub();
        System.out.println(sup.staticGet());
        System.out.println(sup.dynamicGet());
    }
}


三、在(基类)构造器内部调用正在构造的对象的动态绑定方法
这个听起来有点拗口,但是这种情况确实会带来一些难以预料的问题。先看一下复杂对象(包含继承、组合)调用构造器的顺序:
1. 调用基类构造器。这个步骤会递归下去,首先从最上层类开始,依次向下。
2. 按声明顺序调用成员的初始化方法,进行成员定义时的初始化。
3. 调用导出类构造器的主体。
若在基类构造器中调用一个动态绑定方法,就要用到那个方法的被重写后的定义。然而,被覆盖的方法在对象被完全构造之前就被调用,可能会发生一些逻辑上的错误。举个例子说,若该重写的方法操纵了一个成员变量,这个成员变量很可能还未进行初始化,这种情况下导致的问题往往不容易被发现。看一个例子:
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;
    public 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);
    }
}
Output:
Glyph() before draw()
RoundGlyph.draw(), radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius=5

Glyph.draw()在RoundGlyph中被重写,但是Glyph的构造器会调用这个方法,直接导致了对RoundGlyph.draw()的调用。但是我们观察输出结果时,radius不是默认的初始值1,而是0,这是因为基类Glyph的构造器调用时,导出类RoundGlyph的成员甚至没有执行定义时的初始化。这显然不是我们要的结果,编译器也不会报错。
因此,编写构造器时的准则是: 用尽可能简单的方法使对象进入正常状态,如果可以的话,避免调用其他方法。在基类构造器内唯一能够安全调用的方法是基类中的final方法和private方法,因为这些方法不能被重写,所以就不会出现上述的问题


四、协变返回类型
从Java SE5以后,支持协变返回类型。它表示在导出类中被重写的方法可以返回基类方法的返回值的导出类对象。
eg.
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);
    }
}
Output:
Grain
Wheat

在Java SE5以前,将强制WheatMill.process()方法返回Grain对象,而不是Wheat,尽管Wheat由Grain继承而来。事实上返回Mill也应该是一种合法的返回类型,因此协变返回类型允许返回更具体的Wheat类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值