Java编程思想7——多态

对于面向对象的程序设计语言,多态是第三种最基本的特征(前两种是数据抽象(封装)和继承)。

多态(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。

1.向上转型(上溯造型)Upcasting

1.1 为什么要上溯造型

但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的子类,岂不是会简单得多?也就是说,如果我们能不顾子类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。

2.深入理解

2.1 方法调用的绑定

将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”(静态绑定)。

后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。

Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。

2.2 产生正确的行为

Shape s = new Circle();
s.draw();

2.3 扩展性

在一个设计良好的OOP程序中,我们的大多数或者所有方法都会遵从tune()的模型,而且只与基础类接口通信。我们说这样的程序具有“扩展性”,因为可以从通用的基础类继承新的数据类型,从而新添一些功能。

多态是一种至关重要的技术,它允许程序员“将发生改变的东西同没有发生改变的东西区分开”。

3.覆盖与重载

子类覆盖父类方法时,构型需要相同,参数等需要一致,才被认为是覆盖。否则编译器会认为是重载。

4.抽象类和方法

包含了抽象方法的一个类叫作“抽象类”。如果一个类里包含了一个或多个抽象方法,类就必须指定成abstract(抽象)。

如果从一个抽象类继承,而且想生成新类型的一个对象,就必须为基础类中的所有抽象方法提供方法定义。如果不这样做(完全可以选择不做),则衍生类也会是抽象的,而且编译器会强迫我们用abstract关键字标志那个类的“抽象”本质。

即使不包括任何abstract方法,亦可将一个类声明成“抽象类”。如果一个类没必要拥有任何抽象方法,而且我们想禁止那个类的所有实例,这种能力就会显得非常有用。

5.接口

“interface”(接口)关键字使抽象的概念更深入了一层。我们可将其想象为一个“纯”抽象类。它允许创建者规定一个类的基本形式:方法名、自变量列表以及返回类型,但不规定方法主体。接口也包含了基本数据类型的数据成员,但它们都默认为static和final。接口只提供一种形式,并不提供实施的细节。

常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言采用了一个名为“protocol”(协议)的关键字,它做的便是与接口相同的事情。

为创建一个接口,请使用interface关键字。为了生成与一个特定的接口(或一组接口)相符的类,要使用implements关键字。

可决定将一个接口中的方法声明明确定义为“public”。但即便不明确定义,它们也会默认为public。

5.1 Java的“多重继承”

在一个子类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于implements关键字的后面,并用逗号分隔它们。

接口最关键的作用,也是使用接口最重要的一个原因:能上溯造型至多个基础类。使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口。

这样便带来了一个问题:到底应该使用一个接口还是一个抽象类呢?若使用接口,我们可以同时获得抽象类以及接口的好处。所以假如想创建的基础类没有任何方法定义或者成员变量,那么无论如何都愿意使用接口,而不要选择抽象类。事实上,如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类。

5.2 通过继承扩展接口

利用继承技术,可方便地为一个接口添加新的方法声明,也可以将几个接口合并成一个新接口。在这两种情况下,最终得到的都是一个新接口。

5.3 常数分组

由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。(没有类型安全性)。

枚举类可以实现类型安全性。

5.4 初始化接口中的字段

接口中定义的字段会自动具有static和final属性。它们不能是“空白final”,但可初始化成非常数表达式。

当然,字段并不是接口的一部分,而是保存于那个接口的static存储区域中。

6.内部类

将一个类定义置入另一个类定义中。这就叫作“内部类”。内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。然而,我们必须认识到内部类与以前讲述的组合方法存在着根本的区别。

若想在除外部类非static方法内部之外的任何地方生成内部类的一个对象,必须将那个对象的类型设为“外部类名.内部类名”。

6.1 内部类和上溯造型

当我们准备上溯造型到一个基础类(特别是到一个接口)的时候,内部类就开始发挥其关键作用。这是由于内部类随后可完全进入不可见或不可用状态——对任何人都将如此。可以非常方便地隐藏实施细节。我们得到的全部回报就是一个基础类或者接口的句柄,而且甚至有可能不知道准确的类型。

普通(非内部)类不可设为private或protected——只允许public或者“友好的”。

6.2 方法和作用域中的内部类

在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做:

  • (1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
  • (2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。

六种情况:

  • (1) 在一个方法内定义的类
    不能从方法外部访问内部类。
  • (2) 在方法的一个作用域内定义的类
    内部类除了访问受限外,其他和一个普通类并没有什么区别。
  • (3) 一个匿名类,用于实现一个接口
    Contents是个接口。创建从Contents衍生出来的匿名类的一个对象:
return new Contents() {
  private int i = 11;
  public int value() { return i; }
};
  • (4) 一个匿名类,用于扩展拥有非默认构建器的一个类
    匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。
public Wrapping wrap(int x) {
  // Base constructor call:
  return new Wrapping(x) {
    public int value() {
      return super.value() * 47;
    }
  }; // Semicolon required
}
  • (5) 一个匿名类,用于执行字段初始化
    若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。
public Destination dest(final String dest) {
  return new Destination() {
    private String label = dest;
    public String readLabel() { return label; }
  };
}
  • (6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)
    只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样操作呢?
    一个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进行重载处理,所以只能拥有这些构建器的其中一个。
public Destination dest(final String dest, final float price) {
  return new Destination() {
    private int cost;
    // Instance initialization for each object:
    {
      cost = Math.round(price);
      if(cost > 100)
        System.out.println("Over budget!");
    }
    private String label = dest;
    public String readLabel() { return label; }
  };
}

6.3 链接到外部类

迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。

另一个重要的事实:创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个引用。所以它们能访问那个封装对象的成员——毋需取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限。

6.4 static内部类

static内部类意味着:

  • (1) 为创建一个static内部类的对象,我们不需要一个外部类对象。
  • (2) 不能从static内部类的一个对象中访问一个外部类对象。

存在一些限制:由于static成员只能位于一个类的外部级别,所以内部类不可拥有static数据或static内部类。
倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。

通常不在一个接口里设置任何代码,但static内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规则——static内部类只位于接口的命名空间内部。

6.5 引用外部类对象

若想生成外部类对象的句柄,就要用一个点号以及一个this来命名外部类。举个例子来说,在Sequence.SSelector类中,它的所有方法都能产生外部类Sequence的存储句柄,方法是采用Sequence.this的形式。

有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象。为达到这个目的,必须在new表达式中提供指向其他外部类对象的一个句柄

Parcel11.Contents c = p.new Contents();

因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个句柄。

6.6 从内部类继承

由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在子类中不再有一个默认的对象(默认的构建器不行)可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联。

class WithInner {
  class Inner {}
}
public class InheritInner extends WithInner.Inner {
  //! InheritInner() {} // Won't compile
  InheritInner(WithInner wi) {
    wi.super();
  }
}

6.7 内部类可以覆盖吗?

若创建一个内部类,然后从封装类继承,并重新定义内部类,那么会出现什么情况呢?
“覆盖”一个内部类——好象它是外部类的另一个方法——这一概念实际不能做任何事情。

然而,仍然有可能“明确”地从内部类继承。就像普通的继承类似。

6.8 内部类标识符

由于每个类都会生成一个.class文件,用于容纳与如何创建这个类型的对象有关的所有信息(这种信息产生了一个名为Class对象的元类)。

内部类:先是封装类的名字,再跟随一个,再跟随内部类的名字。如果内部类是匿名的,那么编译器会简单地生成数字,把它们作为内部类标识符使用。若内部类嵌套于其他内部类中,则它们的名字简单地追加在一个以及外部类标识符的后面。

6.9 为什么要用内部类:控制框架

到目前为止,大家已接触了对内部类的运作进行描述的大量语法与概念。但这些并不能真正说明内部类存在的原因。为什么Sun要如此麻烦地在Java 1.1里添加这样的一种基本语言特性呢?答案就在于我们在这里要学习的“控制框架”。

一个“应用程序框架”是指一个或一系列类,它们专门设计用来解决特定类型的问题。为使用应用程序框架,我们可从一个或多个类继承,并覆盖其中的部分方法。我们在覆盖方法中编写的代码用于定制由那些应用程序框架提供的常规方案,以便解决自己的实际问题。“控制框架”属于应用程序框架的一种特殊类型,受到对事件响应的需要的支配;主要用来响应事件的一个系统叫作“由事件驱动的系统”。

abstract public class Event {
  private long evtTime;
  public Event(long eventTime) {
    evtTime = eventTime;
  }
  public boolean ready() {
    return System.currentTimeMillis() >= evtTime;
  }
  abstract public void action();
  abstract public String description();
}
public class Controller {
  private EventSet es = new EventSet();
  public void addEvent(Event c) { es.add(c); }
  public void run() {
    Event e;
    while((e = es.getNext()) != null) {
      if(e.ready()) {
        e.action();
        System.out.println(e.description());
        es.removeCurrent();
      }
    }
  }
}

注意在迄今为止的所有设计中,我们仍然不能准确地知道一个“事件”要做什么。这正是整个设计的关键;它怎样“将发生变化的东西同没有变化的东西区分开”?或者用我的话来讲,“改变的意图”造成了各类Event对象的不同行动。我们通过创建不同的Event子类,从而表达出不同的行动。

这里正是内部类大显身手的地方。它们允许我们做两件事情:

  • (1) 在单独一个类里表达一个控制框架应用的全部实施细节,从而完整地封装与那个实施有关的所有东西。内部类用于表达多种不同类型的action(),它们用于解决实际的问题。除此以外,后续的例子使用了private内部类,所以实施细节会完全隐藏起来,可以安全地修改。
  • (2) 内部类使我们具体的实施变得更加巧妙,因为能方便地访问外部类的任何成员。若不具备这种能力,代码看起来就可能没那么使人舒服,最后不得不寻找其他方法解决。

思考控制框架的一种具体实施方式,它设计用来控制温室(Greenhouse)功能。每个行动都是完全不同的:控制灯光、供水以及温度自动调节的开与关,控制响铃,以及重新启动系统。但控制框架的设计宗旨是将不同的代码方便地隔离开。对每种类型的行动,都要继承一个新的Event内部类,并在action()内编写相应的控制代码。

public class GreenhouseControls extends Controller {
  private boolean light = false;
  private boolean water = false;
  private String thermostat = "Day";

  private class LightOn extends Event {
    public LightOn(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Put hardware control code here to
      // physically turn on the light.
      light = true;
    }
    public String description() {
      return "Light is on";
    }
  }

  private class LightOff extends Event {
    public LightOff(long eventTime) {
      super(eventTime);
    }
    public void action() {
      // Put hardware control code here to
      // physically turn off the light.
      light = false;
    }
    public String description() {
      return "Light is off";
    }
  }
  ...
}

注意light(灯光)、water(供水)、thermostat(调温)以及rings都隶属于外部类GreenhouseControls,所以内部类可以毫无阻碍地访问那些字段。

请注意内部类看起来为什么总是类似于多重继承:Bell拥有Event的所有方法,而且也拥有外部类GreenhouseControls的所有方法。

7.构建器和多态

7.1 构建器的调用顺序

一个子类只能访问它自己的成员,不能访问基础类的成员(这些成员通常都具有private属性)。只有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对子类的每个部分进行构建器调用的原因。在子类的构建器主体中,若我们没有明确指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。如果不存在默认构建器,编译器就会报告一个错误。

对于一个复杂的对象,构建器的调用遵照下面的顺序:

  • (1) 调用基础类构建器。这个步骤会不断重复下去,首先得到构建的是分级结构的根部,然后是下一个子类,等等。直到抵达最深一层的子类。
  • (2) 按声明顺序调用成员初始化模块。
  • (3) 调用子类构建器的主体。
    此时,所有基础类成员以及当前对象的成员对象均已获得正确的初始化。

7.2 继承和finalize()

7.3 构建器内部的多态方法的行为

若当前位于一个构建器的内部,同时调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?

从概念上讲,构建器的职责是让对象实际进入存在状态。在任何构建器内部,整个对象可能只是得到部分组织——我们只知道基础类对象已得到初始化,但却不知道哪些类已经继承。
如果在构建器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。

初始化的实际过程是这样的:

  • (1) 在采取其他任何操作之前,为对象分配的存储空间初始化成二进制零。
  • (2) 就象前面叙述的那样,调用基础类构建器。此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为0,这是由于步骤(1)造成的。
  • (3) 按照原先声明的顺序调用成员初始化代码。
  • (4) 调用子类构建器的主体。

因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构建器内唯一能够安全调用的是在基础类中具有final属性的那些方法(也适用于private方法,它们自动具有final属性)。这些方法不能被覆盖,所以不会出现上述潜在的问题。

8.通过继承进行设计

组合不会强迫我们的程序设计进入继承的分级结构中。同时,组合显得更加灵活,因为可以动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。

一条常规的设计准则是:用继承表达行为间的差异,并用成员变量(组合)表达状态的变化。

8.1 纯继承与扩展

可将其描述成一种纯粹的“属于”关系,因为一个类的接口已规定了它到底“是什么”或者“属于什么”。通过继承,可保证所有子类都只拥有基础类的接口。如果按上述示意图操作,衍生出来的类除了基础类的接口之外,也不会再拥有其他什么。




也就是说,基础类可接收我们发给子类的任何消息,因为两者拥有完全一致的接口。


“类似于”关系,因为扩展后的子类“类似于”基础类——它们有相同的基础接口——但它增加了一些特性,要求用额外的方法加以实现。
尽管这是一种有用和明智的做法(由具体的环境决定),但它也有一个缺点:子类中对接口扩展的那一部分不可在基础类中使用。所以一旦上溯造型,就不可再调用新方法:


8.2 下溯造型(Downcasting)与运行期类型标识

由于我们在上溯造型(在继承结构中向上移动)期间丢失了具体的类型信息,所以为了获取具体的类型信息——亦即在分级结构中向下移动——我们必须使用 “下溯造型”技术。



为解决这个问题,必须有一种办法能够保证下溯造型正确进行。只有这样,我们才不会冒然造型成一种错误的类型,然后发出一条对象不可能收到的消息。

在Java中,所有造型都会自动得到检查和核实!所以即使我们只是进行一次普通的括弧造型(类型转换),进入运行期以后,仍然会毫无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。如果不是,就会得到一个ClassCastException(类造型违例)。在运行期间对类型进行检查的行为叫作“运行期类型标识”(RTTI)。

RTTI的意义远不仅仅反映在造型处理上。例如,在试图下溯造型之前,可通过一种方法了解自己处理的是什么类型。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 9,159评论 0 14
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,958评论 18 399
  • 践行者老师被刘老师指挥着去取放在地上的塔罗牌时,想想那姿势,那动作,忍不住的想笑。[捂脸]被束缚的手脚,圆圆的脑袋...
    像花儿一样绽放_fb06阅读 1,618评论 0 0
  • 你的眸子没有了露水。 露水在你回头看母亲时, 被黎明拾起。 这天,黎明比太阳晚来了一些, 你低头没有看见光。 那双...
    许之欢喜阅读 1,677评论 0 0
  • 秋雨如幕落骄阳 鸟雀无欢风渐凉 闹意枝头春不再 不悲不喜不远方 草色焉焉待寒霜 乌云绕月裹秋装 触目秋寒思水暖 春...
    倔强的松茸阅读 1,729评论 0 3