第八章 多态(上)

时间:2021-09-27 09:22:54

            第八章  多态(上)

    “我曾经被问道‘求教,Babbage先生,如果你向机器中输入错误的数字,可以得到正确的答案吗?我无法恰当地理解产生这种问题的概念上的混淆”        在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征。        多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代码的组织结构和可读性,还能够创建可扩展的程序——即无论在项目最初创建时还是在需要添加新功能时都可以“生长”的程序。        “封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化”把接口和实现分离开来。多态的作用则是消除类型之间的耦合关系。         在本章中,通过一些基本、简单的例子深入浅出地介绍多态(也称作动态绑定、后期绑定或运行时绑定)。           8.1  再论向上转型          在第七章中我们已经知道,对象既可以作为它自己本身的类型使用,也可以作为它的基本类型使用。而这种把某个对象的引用视为对基类类型的引用的做法被称作向上转型——因为在继承树的画法中,基类是放置在上方的。          但是这样做也存在一个问题,下面看一个例子:          package com.example.testjava;          public enum Note {      MIDDLE_C, C_SHARP, B_FLAT }               package com.example.testjava;        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);
     } }
     package com.example.testjava;      public class Music {
     public static void tune(Instrument i){
          i.play(Note.MIDDLE_C);
     }
     public static void main(String[] args) {
          Wind wind = new Wind();
          tune(wind);
     } }        Music.tune()方法接受一个Instrument引用,同时也接受任何导出自Instrument的类。在main()方法中,当一个Wind引用传递到tune()方法时,就会出现这种情况,而不需要任何类型转换。这样做是允许的——因为Wind从Instrument继承而来,所以Instrument的接口必定存在于Wind中。从Wind向上转型到Instrument可能会“缩小”接口,但不会比Instrument的全部接口更窄。   8.1.1  忘记对象类型           Music。java看起来似乎有些奇怪。为什么所有人都故意忘记对象类型呢?在进行向上转型时,就会产生这种情况;并且如果让tune()方法直接接受一个Wind引用作为自己的参数,似乎会更为直观。虽然这样做可以,但是必须为添加的每一个新Instrument类编写特定类型的方法。最后导致代码量大量增加。
8.2  转机       看下面这段代码中的tune()方法:       public static void tune(Instrument i){
          i.play(Note.MIDDLE_C);
     }        它接受一个Instrument引用。那么在这种情况下,编译器怎样才能知道这个Instrument引用指向的是Wind对象,而不是其他对象呢?实际上,编译器无法得知。为了深入理解这个问题,有必要研究一下绑定这个话题。
8.2.1  方法调用绑定        将一个方法调用同一个方法主体关联起来被称作绑定。若在程序执行前前进行绑定(如果有的话,有编译器和连接程序实现),叫做前期绑定。读者可能以前从来没有听说过这个术语,因为它是面向过程的语言中不需要选择就默认的绑定方式。例如,C只有一种方法调用,那就是前期绑定。        上述程序之所以令人迷惑,主要是因为前期绑定。因为,当编译器只有一个Instrument引用时,它无法知道究竟调用哪个方法才对。         解决的办法就是后期绑定,它的含义就是在运行时根据对象的类型进行绑定。在Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是后期绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定——它会自动发生。        为什么要将某个方法声明为final 呢?正如前一章提到的那样,它可以防止其他人覆盖该方法。但更重要的一点或许是:这样做可以有效地“关闭”动态绑定,或者说,告诉编译器不需要对其进行动态绑定。这样,编译器就可以为final方法调用生成更有效的代码。然而,大多数情况下,这样做对程序的整体性能不会有什么改观。所以,最好根据设计来决定是否使用final,而不是出于试图提高性能的目的来使用final。
8.22  产生正确的行为        一旦知道Java中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有导出类都可以正确运行。或者换一种说法,发送消息给某个对象,让该对象去断定应该做什么事。       在一个包下面建立多个类:package com.example.duotai;       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(){
          int nextInt = rand.nextInt(3);
          switch (nextInt) {
          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();
          }
     }

} 运行结果: Triangle.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Triangle.draw()
Circle.draw()
8.2.3  可扩展性 之前的“乐器”(Instrument)示例,由于有多态机制,我们可以根据自己的需求对系统添加任意多的新类型,而不需要更改tune()方法。
8.2.4  缺陷:“覆盖”私有方法      public class PrivateOverride {      private void f(){           System.out.println("Private f()");      }
     public static void main(String[] args) {
          PrivateOverride po = new Derived();
          po.f();
     }
}
public class Derived extends PrivateOverride {
     public void f(){
          System.out.println("public f()");
     }
}
运行结果:Private f()     我们期望的输出是public f(),但是由于private方法被自动认为是final方法,而且对导出类是屏蔽的。因此,Derived类中的f()方法是一个全新的方法;既然基类中 的f()方法在子类中不可见,因此甚至也不能重载。
8.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 FieldAccess {
     public static void main(String[] args) {
          Super sup = new Sub();
          System.out.println("sup.field="+sup.field);
          System.out.println("sup.getField="+sup.getField());
          Sub sub = new Sub();
          System.out.println("sub.field="+sub.field);
          System.out.println("sub.getField="+sub.getField());
          System.out.println("sub.getSuperfield="+sub.getSuperfield());
         
     }
} 运行结果: sup.field=0
sup.getField=1
sub.field=1
sub.getField=1
sub.getSuperfield=0
从这个例子可以看出,任何域访问操作都将由编译器解析,因此不是多态的。 如果某个方法是静态的,它的行为就不具有多态性。
8.3  构造器和多态       通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该static声明是隐式的),但还是非常有必要理解构造器怎么样通过多态在复杂的层次结构中运作,这一理解将有助于大家避免一些令人不快的困扰。
8.3.1  构造器的调用顺序       基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐向上链接,以使每个基类的构造器都能得到调用。       对象调用构造器需要遵照下面的顺序:
  1. 调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的跟,然后是下一层导出类,等等,直到最底层的导出类。
  2. 按声明顺序调用成员的初始化方法
  3. 调用导出类构造器的主体

8.3.2  继承与清理      通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理。如果遇到清理问题,那么必须用心为新类创建dispose()方法(在这里我选用此名称,读者可以提供更好的)。并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法。当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法;否则,基类的清理动作就不会发生。
8.3.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() before draw()
RoundGlyph.draw().radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph().radius=5
       从结果上可以看到,RoundGlyph.draw().radius=0,这就是因为在基类的构造方法中调用了导出类的方法,但是导出类的成员还没有被初始化。所以为零。整个程序看起来逻辑上没什么问题,而且编译也没有出错,但是结果却出乎意料,所以这里一定要注意,在构造器中唯一能安全调用的那些方法是基类中的final方法(也适用于private方法,它们自动属于final方法)。这些方法不能被覆盖,因此也就不会出现上述令人惊讶的问题。