Java编程思想-第8章 多态(要点)

  我曾经被问到‘求教, Babbage先生,如果你向机器中输入错误的数字,可以得到正确的 答案吗?’我无法恰当地理解产生这种问题的概念上的混淆。

 Charles Babbage(1791-1871)

  在面向对象的程序设计语言中,多态是继数据抽象和继承之后的第三种基本特征 多态通过分离做什么和怎么做,从另一角度将接口和实现分离开来。多态不但能够改善代 码的组织结构和可读性,还能够创建可扩展的程序—即无论在项目最初创建时还是在需要添 加新功能时都可以“生长”的程序 封装”通过合并特征和行为来创建新的数据类型。“实现隐藏”则通过将细节“私有化” 把接口和实现分离开来。这种类型的组织机制对那些拥有过程化程序设计背景的人来说,更容 易理解。而多态的作用则是消除类型之间的耦合关系。在前一章中我们已经知道,继承允许将 对象视为它自己本身的类型或其基类型来加以处理。这种能力极为重要,因为它允许将多种类 型(从同一基类导出的)视为同一类型来处理,而同一份代码也就可以毫无差别地运行在这些 不同类型之上了。多态方法调用允许一种类型表现出与其他相似类型之间的区别,只要它们都 是从同一基类导出而来的。这种区别是根据方法行为的不同而表示出来的,虽然这些方法都可 以通过同一个基类来调用。

8.1 再论向上转型

8.1.1 忘记对象类型

8.2 转机

8.2.1 方法调用绑定

8.2.2 产生正确的行为
  且知道Jav中所有方法都是通过动态绑定实现多态这个事实之后,我们就可以编写只与 基类打交道的程序代码了,并且这些代码对所有的导出类都可以正确运行。或者换一种说法 发送消息给某个对象,让该对象去断定应该做什么事。

8.2.3 可扩展性

8.2.4 缺陷:“覆盖” 私有方法

8.2.5 缺陷:域与静态方法

  旦你了解了多态机制,可能就会开始认为所有事物都可以多态地发生。然而,只有普通 的方法调用可以是多态的。例如,如果你直接访问某个域,这个访问就将在编译期进行解析, 就像下面的示例所演示的:

//: polymorphism/FieldAccess.java

// Direct field access is determined at compile time.

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

  }

}/*Output:

sup.Field = 0, sup.getField() = 1

sub.Field = 1, sub.getField() = 1, sub.getSuperField() = 0

*///:

  当Sub对象转型为 Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。在 本例中,为 Super.field和 Sub.field分配了不同的存储空间。这样,Sub实际上包含两个称为field 的域:它自己的和它从 Super处得到的。然而,在引用Sub中的field时所产生的默认域并非 Super 版本的field域。因此,为了得到 Super.field,必须显式地指明 super.field 尽管这看起来好像会成为一个容易令人混淆的问题,但是在实践中,它实际上从来不会发 生。首先,你通常会将所有的域都设置成 private,因此不能直接访问它们,其副作用是只能调 用方法来访问。另外,你可能不会对基类中的域和导出类中的域赋予相同的名字,因为这种做 法容易令人混淆。

8.3 构造器和多态

  通常,构造器不同于其他种类的方法。涉及到多态时仍是如此。尽管构造器并不具有多态 性(它们实际上是 static方法,只不过该 Estatic声明是隐式的),但还是非常有必要理解构造器怎 样通过多态在复杂的层次结构中运作,这一理解将有助于大家避免一些令人不快的困扰

8.3.1 构造器的调用顺序

1)调用基类构造器。这个步骤会不断地反复递归下去,首先是构造这种层次结构的根,然 后是下一层导出类,等等,直到最低层的导出类。

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

3)调用导出类构造器的主体。

8.3.2 继承与清理

8.3.3 构造器内部的多态方法的行为

  构造器调用的层次结构带来了一个有趣的两难问题。如果在一个构造器的内部调用正在构 造的对象的某个动态绑定方法,那会发生什么情况呢? 在一般的方法内部,动态绑定的调用是在运行时才决定的,因为对象无法知道它是属于方 法所在的那个类,还是属于那个类的导出类 如果要调用构造器内部的一个动态绑定方法,就要用到那个方法的被覆盖后的定义。然而, 这个调用的效果可能相当难于预料,因为被覆盖的方法在对象被完全构造之前就会被调用。这可 能会造成一些难于发现的隐藏错误 从概念上讲,构造器的工作实际上是创建对象(这并非是一件平常的工作)。在任何构造器 内部,整个对象可能只是部分形成—一我们只知道基类对象已经进行初始化。如果构造器只是 在构建对象过程中的一个步骤,并且该对象所属的类是从这个构造器所属的类导出的,那么导 出部分在当前构造器正在被调用的时刻仍旧是没有被初始化的。然而,一个动态绑定的方法调 用却会向外深入到继承层次结构内部,它可以调用导出类里的方法。如果我们是在构造器内部 这样做,那么就可能会调用某个方法,而这个方法所操纵的成员可能还未进行初始化——这肯 定会招致灾难 通过下面这个例子,我们会看到问题所在:

//: polymorphism/PolyConstructors.java

// Constructors and polymorphism / don't produce what you might expect.

import static net.mindview.util.Print.*;

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.draw(), radius = 0

Glyph() after draw()

RoundGlyph.RoundGlyph(), radius = 5

*///:

   Glyph.draw()方法设计为将要被覆盖,这种覆盖是在 RoundGlyph中发生的。但是Glyph构 造器会调用这个方法,结果导致了对 RoundGlyph.draw()的调用,这看起来似乎是我们的目的。 但是如果看到输出结果,我们会发现当 Glyph的构造器调用 draw()方法时, radius不是默认初始 值1,而是0。这可能导致在屏幕上只画了一个点,或者根本什么东西都没有;我们只能干瞪眼, 并试图找出程序无法运转的原因所在。 前一节讲述的初始化顺序并不十分完整,而这正是解决这一谜题的关键所在。初始化的实 际过程是:

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

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

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

4)调用导出类的构造器主体。 这样做有一个优点,那就是所有东西都至少初始化成零(或者是某些特殊数据类型中与 零”等价的值),而不是仅仅留作垃圾。其中包括通过“组合”而嵌入一个类内部的对象引用, 其值是nul所以如果忘记为该引「用进行初始化,就会在运行时出现异常。查看输出结果时,会 发现其他所有东西的值都会是零,这通常也正是发现问题的证据。

8.4 协变返回类型

8.5 用继承进行设计 学习了多态之后,看起来似乎所有东西都可以被继承,因为多态是一种如此巧妙的工具。 事实上,当我们]使用现成的类来建立新类时,如果首先考虑使用继承技术,反倒会加重我们的 设计负担,使事情变得不必要地复杂起来。 更好的方式是首先选择“组合”,尤其是不能十分确定应该使用哪一种方式时。组合不会强 制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择类型 (因此也就选择了行为);相反,继承在编译时就需要知道确切类型。

8.5.1 纯继承与扩展

8.5.2 向下转型与运行时类型识别 (RTTI)

8.6 总结 多态意味着“不同的形式”。在面向对象的程序设计中,我们持有从基类继承而来的相同接口, 以及使用该接口的不同形式:不同版本的动态绑定方法。

posted @ 2018-03-02 15:39  JackYgy  阅读(100)  评论(0)    收藏  举报