第八章 - 多态

向上转型

“封装”通过合并特性和行为来创建新的数据类型。而“多态”的作用则是消除类型之间的耦合关系。继承允许将对象视为它自身的类型或是基类类型来处理。

    对象既可以作为它本身的类型使用,也可以作为他的基类类型使用。而这种把某个对象的引用视为其基类类型引用的做法叫做“向上转型”。

    但是这样做也会有一个问题,具体看下面有关乐器的例子,首先,既然几个例子都是演奏乐符(Note),我们就创建一个Note类。

public enum Note {
    MIDDLE_C, C_SHARP, B_FLAT; 
} 

在这里Wind是一种Instrument,因为可以从Instrument类继承。

class Instrument {
  public void play(Note n) {
    System.out.println("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()方法接受一个Instrnment引用,同时也接受任何导出自Instrnment的类。在main()方法中,当一个Wind引用传递给tune()方法时 就会出现这种情况,而不需要任何类型转换。这样做事允许的--因为Wind是从Instrnment继承来的,所以Instrnment的接口必定存在于Wind中。从Wind向上转到Instrnment可能会缩小接口,但不会比Instrnment的全部接口更窄。

忘记对象类型

  Music.java看起来有点奇怪,为什么会故意忘记对象类型呢?在向上转型是会产生这种情况:如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎更直观。但这样会引发一个重要的问题:如果这样做,就需要为系统内Instrnment的每种类型都编写一个新的tune()方法。假设按照这种逻辑,现在加入Stringed(弦乐)和Brass(管乐)这两种Instrnment(乐器)。

    
class Stringed extends Instrument {
  public void play(Note n) {
    System.out.println("Stringed.play() " + n);
  }
}

class Brass extends Instrument {
  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); 
    tune(violin);
    tune(frenchHorn);
  }
}

    这样做行得通,但缺点:必须为每一个Instrnment的子类编写特定的方法。这意味着需要更多的编写工作。

而且如果以后想添加类似tune()的新方法,扔需要大量的工作。此外,如果我们忘记重载某个方法,编译器不会返回任何的错误信息。这样关于类型的整个处理过程就变动难以操纵。

    如果我们只写一个方法,它仅接受基类作为参数,而不是那些子类。这样只是与基类打交道,这样会更好一点,这也正式多态允许的。

转机

    观察一下tnue()方法,

public static void tune(Instrument i) {
  // ...
  i.play(Note.MIDDLE_C);
}

    它接受的是一个Instrument引用,那么这种情况下,编译器怎么知道这Instrument的引用是Wind对象还是Brass对象还是Stringed对象呢?实际上,编译器无法得知,为了深入理解这个问题,有必要研究一下“绑定”这个话题

方法调用绑定

    将一个方法调用同一个方法主题关联起来成为绑定。若在程序执行前进行绑定,叫做前期绑定。

    上述程序之所以让人迷惑,主要是因为前期绑定。因为编译器只有一个Instrument引用,它无法知道究竟调用哪个方法才对。

    解决方法就是后期绑定,他 含义就是在运行时根据对象的类型进行绑定。

产生正确的行为

    我们知道java的所有方法都是通过动态绑定实现多态这件事之后,我们就可以编写只与基类打交道的代码了。

    有一个基类Shape,以及多个导出类Circle,Square,Triangle等,向上转型就可以像下面这条语句这么简单:

    Shape s = new Circle();

    这里创建了一个Circle对象,并把得到的引用立即赋值给了Shape,这样做看似错误(将一个类型赋值给另一个类型)。但实际上没问题,因为通过继承,Circle就是一种Shape。
    假设调用一个基类方法(它已在导出类中被覆盖)   ,s.draw();你可能认为调用的是Shape的draw() ,因为这毕竟是一个Shape的引用,那么编译器是怎么知道的呢?由于动态绑定(多态),还是正确调用了Circle.draw()方法。

    下面的一个例子:对draw()方法的调用都是通过动态绑定进行的。

public class Shape {
  public void draw() {}
  public void erase() {}
} 
public class Circle extends Shape {
  public void draw() {
    System.out.println("Circle.draw()");
  }
  public void erase() {
    System.out.println("Circle.erase()");
  }
}

public class Square extends Shape {
  public void draw() {
    System.out.println("Square.draw()");
  }
  public void erase() {
    System.out.println("Square.erase()");
  }
}
public class Triangle extends Shape {
  public void draw() {
    System.out.println("Triangle.draw()");
  }
  public void erase() {
    System.out.println("Triangle.erase()");
  }
}

public class RandomShapeGenerator {
  private Random rand = new Random(47);
  public Shape next() {
    switch(rand.nextInt(3)) {
      default:
      case 0: return new Circle();
      case 1: return new Square();
      case 2: return new Triangle();
    }
  }
}
public class Shapes {
  private static RandomShapeGenerator gen = new RandomShapeGenerator();
  public static void main(String[] args) {
    Shape[] s = new Shape[9];

    for(int i = 0; i < s.length; i++)
      s[i] = gen.next();

    for(Shape shp : s)
      shp.draw();
  }
}

缺陷“覆盖”私有方法

    

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()");
  }
}

    我们所期望的是输出public f(),但是由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此在这种情况下,Derived类中的f()方法就是一个全新的方法

    结论就是:只有非private方法才可以被覆盖。

   缺陷 域与静态方法

    
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 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());
  }
}

当Sub对象转型为Super引用时,任何域访问操作都将由编译器编译,因此不是多态的,本例子中为Super.field和Sub.field分配了不同的存储空间。这样,Sub实际上包含了两个称为field的域:它自己喝从Super处得到的。然而,再引用Sub中的field时所产生的默认的并非Super版本的field域,因为,为了得到Super.field,必须显示的指明Super.field。

    比如某个方法是静态的,它的行为就不具有多态性。

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(); // Upcast
    System.out.println(sup.staticGet());
    System.out.println(sup.dynamicGet());
  }
}

   静态方法是与类,并非单个对象关联的。

构造器和多态

    通常, 构造器不同于其他种类的方法。涉及到多态时仍是如此,尽管构造器并不具有多态性(它们实际上static方法,static的声明是隐式的),还是有必要理解构造器怎么通过多态在复杂的层次结构中运作。

构造器的调用顺序

   
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();
  }
}

单纯构造器的调用顺序

    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;
  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);
  }
}
          

    Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的,但是Glyph构造器会调用这个方法,结果导致了RoundGlyph.draw()的调用,这看起来是我们的目的。但是如果看输出结果,我们会发现Glyph的构造器在调用draw()方法是,radius不是默认的初始值1,而是0。

   所以前面的初始化顺便并不是十分完整,初始化的实际过程是:

    1)在其他任何事物发生之前,将分配给对象的存储空间初始值为二进制的零。

    2)如前所述那种调用基类构造器,此时,调用被覆盖后的draw()方法,由于步骤1的缘故,我们此时会发现radius的值为0

    3)按照声明的顺序调用成员的初始化方法。

    4)调用导出类的构造器。

协变返回类型

    java se5 添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的的某种导出类型。

    
class Grain {
  public String toString() {
    return "Grain";
  }
}

class Wheat extends Grain {
  public String toString() {
    return "Wheat";
  }
}

class Mill {
  Grain process() {
    return new Grain();
  }
}

class WheatMill extends Mill {
  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);
  }
}
    
    java se5与java之前的版本主要差异就是:早版本会强制process()的覆盖版本必须返回Grain,而不能返回Wheat,尽管Wheat是从Grain导出的,因为也应该是一种合法的返回类型。协变返回类型允许返回更具体的Wheat类型。
    

 向下转型与运行时类型识别

   由于向上转型会丢失具体的类型信息,所以我们就想,通过向下转型获取类型信息。我们必须通过一个特殊的操作来获得安全的向下转型(加括弧的形式进行类型转化)。在进入运行期的仍会进行类型检查,以确保使我们希望的类型,如果不是,就会返回异常(ClassCastException)     

    

class Useful {
  public void f() {}
  public void g() {}
}

class MoreUseful extends Useful {
  public void f() {}
  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();

    ((MoreUseful)x[1]).u(); 
    ((MoreUseful)x[0]).u(); // Exception thrown
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值