Java编程基础之多形性

来源:互联网 发布:五大流氓 知乎 编辑:程序博客网 时间:2024/04/29 09:22
“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承。”

“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。

通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多形性却涉及对“类型”的分解。

1、上溯造型

   取得一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——因为继承树的画法是基础类位于最上方。
但这样做也会遇到一个问题,如下例所示:

  

class Note {  private int value;  private Note(int val) { value = val; }  public static final Note    middleC = new Note(0),     cSharp = new Note(1),    cFlat = new Note(2);} // Etc.class Instrument {  public void play(Note n) {    System.out.println("Instrument.play()");  }}// Wind objects are instruments// because they have the same interface:class Wind extends Instrument {  // Redefine interface method:  public void play(Note n) {    System.out.println("Wind.play()");  }}public class Music {  public static void tune(Instrument i) {    // ...    i.play(Note.middleC);  }  public static void main(String[] args) {    Wind flute = new Wind();    tune(flute); // Upcasting  }} ///:~

其中,方法Music.tune()接收一个Instrument句柄,同时也接收从Instrument衍生出来的所有东西。当一个Wind句柄传递给tune()的时候,就会出现这种情况。此时没有造型的必要。这样做是可以接受的;Instrument里的接口必须存在于Wind中,因为Wind是从Instrument里继承得到的。从Wind向Instrument的上溯造型可能“缩小”那个接口,但不可能把它变得比Instrument的完整接口还要小。

  1.1 为什么要上溯造型

      这个程序看起来也许显得有些奇怪。为什么所有人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。而且如果让tune()简单地取得一个Wind句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做,就需为系统内Instrument的每种类型写一个全新的tune()。假设按照前面的推论,加入Stringed(弦乐)和Brass(铜管)这两种Instrument(乐器):

//: Music2.java // Overloading instead of upcastingclass Note2 {  private int value;  private Note2(int val) { value = val; }  public static final Note2    middleC = new Note2(0),     cSharp = new Note2(1),    cFlat = new Note2(2);} // Etc.class Instrument2 {  public void play(Note2 n) {    System.out.println("Instrument2.play()");  }}class Wind2 extends Instrument2 {  public void play(Note2 n) {    System.out.println("Wind2.play()");  }}class Stringed2 extends Instrument2 {  public void play(Note2 n) {    System.out.println("Stringed2.play()");  }}class Brass2 extends Instrument2 {  public void play(Note2 n) {    System.out.println("Brass2.play()");  }}public class Music2 {  public static void tune(Wind2 i) {    i.play(Note2.middleC);  }  public static void tune(Stringed2 i) {    i.play(Note2.middleC);  }  public static void tune(Brass2 i) {    i.play(Note2.middleC);  }  public static void main(String[] args) {    Wind2 flute = new Wind2();    Stringed2 violin = new Stringed2();    Brass2 frenchHorn = new Brass2();    tune(flute); // No upcasting    tune(violin);    tune(frenchHorn);  }} ///:~

这样做当然行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。这意味着第一次就要求多得多的编程量。以后,假如想添加一个象tune()那样的新方法或者为Instrument添加一个新类型,仍然需要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示任何错误。这样一来,类型的整个操作过程就显得极难管理,有失控的危险。
但假如只写一个方法,将基础类作为自变量或参数使用,而不是使用那些特定的衍生类,岂不是会简单得多?也就是说,如果我们能不顾衍生类,只让自己的代码与基础类打交道,那么省下的工作量将是难以估计的。
这正是“多形性”大显身手的地方。然而,大多数程序员(特别是有程序化编程背景的)对于多形性的工作原理仍然显得有些生疏。

   1.2 深入理解
对于Music.java的困难性,可通过运行程序加以体会。输出是Wind.play()。这当然是我们希望的输出,但它看起来似乎并不愿按我们的希望行事。请观察一下tune()方法:

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

它接收Instrument句柄。所以在这种情况下,编译器怎样才能知道Instrument句柄指向的是一个Wind,而不是一个Brass或Stringed呢?编译器无从得知。为了深入了理解这个问题,我们有必要探讨一下“绑定”这个主题。
   1.3 方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。若在程序运行以前执行绑定(由编译器和链接程序,如果有的话),就叫作“早期绑定”。大家以前或许从未听说过这个术语,因为它在任何程序化语言里都是不可能的。C编译器只有一种方法调用,那就是“早期绑定”。
上述程序最令人迷惑不解的地方全与早期绑定有关,因为在只有一个Instrument句柄的前提下,编译器不知道具体该调用哪个方法。
解决的方法就是“后期绑定”,它意味着绑定在运行期间进行,以对象的类型为基础。后期绑定也叫作“动态绑定”或“运行期绑定”。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的。但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息。
Java中绑定的所有方法都采用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自动发生的。
为什么要把一个方法声明成final呢?它能防止其他人覆盖那个方法。但也许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不需要进行动态绑定。这样一来,编译器就可为final方法调用生成效率更高的代码。
    1.4 覆盖与过载
现在让我们用不同的眼光来看看本章的头一个例子。在下面这个程序中,方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器允许我们对方法进行过载处理,使其不报告出错。但这种行为可能并不是我们所希望的。下面是这个例子:

//: WindError.java // Accidentally changing the interfaceclass NoteX {  public static final int    MIDDLE_C = 0, C_SHARP = 1, C_FLAT = 2;}class InstrumentX {  public void play(int NoteX) {    System.out.println("InstrumentX.play()");  }}class WindX extends InstrumentX {  // OOPS! Changes the method interface:  public void play(NoteX n) {    System.out.println("WindX.play(NoteX n)");  }}public class WindError {  public static void tune(InstrumentX i) {    // ...    i.play(NoteX.MIDDLE_C);  }  public static void main(String[] args) {    WindX flute = new WindX();    tune(flute); // Not the desired behavior!  }} ///:~

这里还向大家引入了另一个易于混淆的概念。在InstrumentX中,play()方法采用了一个int(整数)数值,它的标识符是NoteX。也就是说,即使NoteX是一个类名,也可以把它作为一个标识符使用,编译器不会报告出错。但在WindX中,play()采用一个NoteX句柄,它有一个标识符n。即便我们使用“play(NoteX NoteX)”,编译器也不会报告错误。这样一来,看起来就象是程序员有意覆盖play()的功能,但对方法的类型定义却稍微有些不确切。然而,编译器此时假定的是程序员有意进行“过载”,而非“覆盖”。请仔细体会这两个术语的区别。“过载”是指同一样东西在不同的地方具有多种含义;而“覆盖”是指它随时随地都只有一种含义,只是原先的含义完全被后来的含义取代了。请注意如果遵守标准的Java命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。
在tune中,“InstrumentX i”会发出play()消息,同时将某个NoteX成员作为自变量使用(MIDDLE_C)。由于NoteX包含了int定义,过载的play()方法的int版本会得到调用。同时由于它尚未被“覆盖”,所以会使用基础类版本。
输出是:
InstrumentX.play()

2、Java的“多重继承”
    接口只是比抽象类“更纯”的一种形式。它的用途并不止那些。由于接口根本没有具体的实施细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有任何办法可以防止多个接口合并到一起。这一点是至关重要的,因为我们经常都需要表达这样一个意思:“x从属于a,也从属于b,也从属于c”。在C++中,将多个类合并到一起的行动称作“多重继承”,而且操作较为不便,因为每个类都可能有一套自己的实施细节。在Java中,我们可采取同样的行动,但只有其中一个类拥有具体的实施细节。所以在合并多个接口的时候,C++的问题不会在Java中重演。如下所示:

在一个衍生类中,我们并不一定要拥有一个抽象或具体(没有抽象方法)的基础类。如果确实想从一个非接口继承,那么只能从一个继承。剩余的所有基本元素都必须是“接口”。我们将所有接口名置于implements关键字的后面,并用逗号分隔它们。可根据需要使用多个接口,而且每个接口都会成为一个独立的类型,可对其进行上溯造型。下面这个例子展示了一个“具体”类同几个接口合并的情况,它最终生成了一个新类:
import java.util.*;interface CanFight {  void fight();}interface CanSwim {  void swim();}interface CanFly {  void fly();}class ActionCharacter {  public void fight() {}}class Hero extends ActionCharacter     implements CanFight, CanSwim, CanFly {  public void swim() {}  public void fly() {}}public class Adventure {  static void t(CanFight x) { x.fight(); }  static void u(CanSwim x) { x.swim(); }  static void v(CanFly x) { x.fly(); }  static void w(ActionCharacter x) { x.fight(); }  public static void main(String[] args) {    Hero i = new Hero();    t(i); // Treat it as a CanFight    u(i); // Treat it as a CanSwim    v(i); // Treat it as a CanFly    w(i); // Treat it as an ActionCharacter  }} ///:~

从中可以看到,Hero将具体类ActionCharacter同接口CanFight,CanSwim以及CanFly合并起来。按这种形式合并一个具体类与接口的时候,具体类必须首先出现,然后才是接口(否则编译器会报错)。
请注意fight()的签名在CanFight接口与ActionCharacter类中是相同的,而且没有在Hero中为fight()提供一个具体的定义。接口的规则是:我们可以从它继承(稍后就会看到),但这样得到的将是另一个接口。如果想创建新类型的一个对象,它就必须是已提供所有定义的一个类。尽管Hero没有为fight()明确地提供一个定义,但定义是随同ActionCharacter来的,所以这个定义会自动提供,我们可以创建Hero的对象。
在类Adventure中,我们可看到共有四个方法,它们将不同的接口和具体类作为自己的自变量使用。创建一个Hero对象后,它可以传递给这些方法中的任何一个。这意味着它们会依次上溯造型到每一个接口。由于接口是用Java设计的,所以这样做不会有任何问题,而且程序员不必对此加以任何特别的关注。
注意上述例子已向我们揭示了接口最关键的作用,也是使用接口最重要的一个原因:能上溯造型至多个基础类。使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序员制作这个类的一个对象,以及规定它仅仅是一个接口。这样便带来了一个问题:到底应该使用一个接口还是一个抽象类呢?若使用接口,我们可以同时获得抽象类以及接口的好处。所以假如想创建的基础类没有任何方法定义或者成员变量,那么无论如何都愿意使用接口,而不要选择抽象类。事实上,如果事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。只有在必须使用方法定义或者成员变量的时候,才应考虑采用抽象类。

 3、常数分组
由于置入一个接口的所有字段都自动具有static和final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum非常相似的效果。如下例所示:

package c07;public interface Months {  int    JANUARY = 1, FEBRUARY = 2, MARCH = 3,     APRIL = 4, MAY = 5, JUNE = 6, JULY = 7,     AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10,    NOVEMBER = 11, DECEMBER = 12;} ///:~

注意根据Java命名规则,拥有固定标识符的static final基本数据类型(亦即编译期常数)都全部采用大写字母(用下划线分隔单个标识符里的多个单词)。
接口中的字段会自动具备public属性,所以没必要专门指定。
现在,通过导入c07.*或c07.Months,我们可以从包的外部使用常数——就象对其他任何包进行的操作那样。此外,也可以用类似Months.JANUARY的表达式对值进行引用。当然,我们获得的只是一个int,所以不象C++的enum那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比,这种(常用的)技术无疑已经是一个巨大的进步。我们通常把“硬编码”数字的行为称为“魔术数字”,它产生的代码是非常难以维护的。
如确实不想放弃额外的类型安全性,可构建象下面这样的一个类(注释):
package c07;public final class Month2 {  private String name;  private Month2(String nm) { name = nm; }  public String toString() { return name; }  public final static Month2    JAN = new Month2("January"),     FEB = new Month2("February"),    MAR = new Month2("March"),    APR = new Month2("April"),    MAY = new Month2("May"),    JUN = new Month2("June"),    JUL = new Month2("July"),    AUG = new Month2("August"),    SEP = new Month2("September"),    OCT = new Month2("October"),    NOV = new Month2("November"),    DEC = new Month2("December");  public final static Month2[] month =  {    JAN, JAN, FEB, MAR, APR, MAY, JUN,    JUL, AUG, SEP, OCT, NOV, DEC  };  public static void main(String[] args) {    Month2 m = Month2.JAN;    System.out.println(m);    m = Month2.month[12];    System.out.println(m);    System.out.println(m == Month2.DEC);    System.out.println(m.equals(Month2.DEC));  }} ///:~

:是Rich Hoffarth的一封E-mail触发了我这样编写程序的灵感。

这个类叫作Month2,因为标准Java库里已经有一个Month。它是一个final类,并含有一个private构建器,所以没有人能从它继承,或制作它的一个实例。唯一的实例就是那些final static对象,它们是在类本身内部创建的,包括:JAN,FEB,MAR等等。这些对象也在month数组中使用,后者让我们能够按数字挑选月份,而不是按名字(注意数组中提供了一个多余的JAN,使偏移量增加了1,也使December确实成为12月)。在main()中,我们可注意到类型的安全性:m是一个Month2对象,所以只能将其分配给Month2。在前面的Months.java例子中,只提供了int值,所以本来想用来代表一个月份的int变量可能实际获得一个整数值,那样做可能不十分安全。
这儿介绍的方法也允许我们交换使用==或者equals(),就象main()尾部展示的那样。

   3.1 初始化接口中的字段
接口中定义的字段会自动具有static和final属性。它们不能是“空白final”,但可初始化成非常数表达式。例如:
import java.util.*;public interface RandVals {  int rint = (int)(Math.random() * 10);  long rlong = (long)(Math.random() * 10);  float rfloat = (float)(Math.random() * 10);  double rdouble = Math.random() * 10;} ///:~

由于字段是static的,所以它们会在首次装载类之后、以及首次访问任何字段之前获得初始化。下面是一个简单的测试:
public class TestRandVals {  public static void main(String[] args) {    System.out.println(RandVals.rint);    System.out.println(RandVals.rlong);    System.out.println(RandVals.rfloat);    System.out.println(RandVals.rdouble);  }} ///:~

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

4、内部类
在Java 1.1中,可将一个类定义置入另一个类定义中。这就叫作“内部类”。内部类对我们非常有用,因为利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在另一个类里的“可见性”。然而,我们必须认识到内部类与以前讲述的“合成”方法存在着根本的区别。
通常,对内部类的需要并不是特别明显的,至少不会立即感觉到自己需要使用内部类。
创建内部类的过程是平淡无奇的:将类定义置入一个用于封装它的类内部:

package c07.parcel1;public class Parcel1 {  class Contents {    private int i = 11;    public int value() { return i; }  }  class Destination {    private String label;    Destination(String whereTo) {      label = whereTo;    }    String readLabel() { return label; }  }  // Using inner classes looks just like  // using any other class, within Parcel1:  public void ship(String dest) {    Contents c = new Contents();    Destination d = new Destination(dest);  }    public static void main(String[] args) {    Parcel1 p = new Parcel1();    p.ship("Tanzania");  }} ///:~

若在ship()内部使用,内部类的使用看起来和其他任何类都没什么分别。在这里,唯一明显的区别就是它的名字嵌套在Parcel1里面。但大家不久就会知道,这其实并非唯一的区别。
更典型的一种情况是,一个外部类拥有一个特殊的方法,它会返回指向一个内部类的句柄。就象下面这样:
package c07.parcel2;public class Parcel2 {  class Contents {    private int i = 11;    public int value() { return i; }  }  class Destination {    private String label;    Destination(String whereTo) {      label = whereTo;    }    String readLabel() { return label; }  }  public Destination to(String s) {    return new Destination(s);  }  public Contents cont() {     return new Contents();   }  public void ship(String dest) {    Contents c = cont();    Destination d = to(dest);  }    public static void main(String[] args) {    Parcel2 p = new Parcel2();    p.ship("Tanzania");    Parcel2 q = new Parcel2();    // Defining handles to inner classes:    Parcel2.Contents c = q.cont();    Parcel2.Destination d = q.to("Borneo");  }} ///:~

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

5、方法和作用域中的内部类
至此,我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,通常表达的都是“单纯”的内部类,非常简单,且极易理解。然而,内部类的设计非常全面,不可避免地会遇到它们的其他大量用法——假若我们在一个方法甚至一个任意的作用域内创建内部类。有两方面的原因促使我们这样做:
(1) 正如前面展示的那样,我们准备实现某种形式的接口,使自己能创建和返回一个句柄。
(2) 要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。同时不愿意把它公开。

在下面这个例子里,将修改前面的代码,以便使用:
(1) 在一个方法内定义的类
(2) 在方法的一个作用域内定义的类
(3) 一个匿名类,用于实现一个接口
(4) 一个匿名类,用于扩展拥有非默认构建器的一个类
(5) 一个匿名类,用于执行字段初始化
(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)

所有这些都在innerscopes包内发生。首先,来自前述代码的通用接口会在它们自己的文件里获得定义,使它们能在所有的例子里使用:

//: Destination.javapackage c07.innerscopes;interface Destination {  String readLabel();} ///:~

由于我们已认为Contents可能是一个抽象类,所以可采取下面这种更自然的形式,就象一个接口那样:
//: Contents.javapackage c07.innerscopes;interface Contents {  int value();} ///:~

尽管是含有具体实施细节的一个普通类,但Wrapping也作为它所有衍生类的一个通用“接口”使用:
//: Wrapping.javapackage c07.innerscopes;public class Wrapping {  private int i;  public Wrapping(int x) { i = x; }  public int value() { return i; }} ///:~

在上面的代码中,我们注意到Wrapping有一个要求使用自变量的构建器,这就使情况变得更加有趣了。
第一个例子展示了如何在一个方法的作用域(而不是另一个类的作用域)中创建一个完整的类
//: Parcel4.java// Nesting a class within a methodpackage c07.innerscopes;public class Parcel4 {  public Destination dest(String s) {    class PDestination        implements Destination {      private String label;      private PDestination(String whereTo) {        label = whereTo;      }      public String readLabel() { return label; }    }    return new PDestination(s);  }  public static void main(String[] args) {    Parcel4 p = new Parcel4();    Destination d = p.dest("Tanzania");  }} ///:~

PDestination类属于dest()的一部分,而不是Parcel4的一部分(同时注意可为相同目录内每个类内部的一个内部类使用类标识符PDestination,这样做不会发生命名的冲突)。因此,PDestination不可从dest()的外部访问。请注意在返回语句中发生的上溯造型——除了指向基础类Destination的一个句柄之外,没有任何东西超出dest()的边界之外。当然,不能由于类PDestination的名字置于dest()内部,就认为在dest()返回之后PDestination不是一个有效的对象。
下面这个例子展示了如何在任意作用域内嵌套一个内部类:
//: Parcel5.java// Nesting a class within a scopepackage c07.innerscopes;public class Parcel5 {  private void internalTracking(boolean b) {    if(b) {      class TrackingSlip {        private String id;        TrackingSlip(String s) {          id = s;        }        String getSlip() { return id; }      }      TrackingSlip ts = new TrackingSlip("slip");      String s = ts.getSlip();    }    // Can't use it here! Out of scope:    //! TrackingSlip ts = new TrackingSlip("x");  }  public void track() { internalTracking(true); }  public static void main(String[] args) {    Parcel5 p = new Parcel5();    p.track();  }} ///:~

TrackingSlip类嵌套于一个if语句的作用域内。这并不意味着类是有条件创建的——它会随同其他所有东西得到编译。然而,在定义它的那个作用域之外,它是不可使用的。除这些以外,它看起来和一个普通类并没有什么区别。
下面这个例子看起来有些奇怪:
//: Parcel6.java// A method that returns an anonymous inner classpackage c07.innerscopes;public class Parcel6 {  public Contents cont() {    return new Contents() {      private int i = 11;      public int value() { return i; }    }; // Semicolon required in this case  }  public static void main(String[] args) {    Parcel6 p = new Parcel6();    Contents c = p.cont();  }} ///:~

cont()方法同时合并了返回值的创建代码,以及用于表示那个返回值的类。除此以外,这个类是匿名的——它没有名字。而且看起来似乎更让人摸不着头脑的是,我们准备创建一个Contents对象:
return new Contents()
但在这之后,在遇到分号之前,我们又说:“等一等,让我先在一个类定义里再耍一下花招”:

return new Contents() {
private int i = 11;
public int value() { return i; }
};

这种奇怪的语法要表达的意思是:“创建从Contents衍生出来的匿名类的一个对象”。由new表达式返回的句柄会自动上溯造型成一个Contents句柄。匿名内部类的语法其实要表达的是:

class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents();

在匿名内部类中,Contents是用一个默认构建器创建的。下面这段代码展示了基础类需要含有自变量的一个构建器时做的事情:
//: Parcel7.java// An anonymous inner class that calls the // base-class constructorpackage c07.innerscopes;public class Parcel7 {  public Wrapping wrap(int x) {    // Base constructor call:    return new Wrapping(x) {       public int value() {        return super.value() * 47;      }    }; // Semicolon required  }  public static void main(String[] args) {    Parcel7 p = new Parcel7();    Wrapping w = p.wrap(10);  }} ///:~

也就是说,我们将适当的自变量简单地传递给基础类构建器,在这儿表现为在“new Wrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。
在前述的两个例子中,分号并不标志着类主体的结束(和C++不同)。相反,它标志着用于包含匿名类的那个表达式的结束。因此,它完全等价于在其他任何地方使用分号。
若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?由于它是匿名的,没有名字赋给构建器,所以我们不能拥有一个构建器。然而,我们可在定义自己的字段时进行初始化:
//: Parcel8.java// An anonymous inner class that performs // initialization. A briefer version// of Parcel5.java.package c07.innerscopes;public class Parcel8 {  // Argument must be final to use inside   // anonymous inner class:  public Destination dest(final String dest) {    return new Destination() {      private String label = dest;      public String readLabel() { return label; }    };  }  public static void main(String[] args) {    Parcel8 p = new Parcel8();    Destination d = p.dest("Tanzania");  }} ///:~

若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的自变量设为final的原因。如果忘记这样做,就会得到一条编译期出错提示。
只要自己只是想分配一个字段,上述方法就肯定可行。但假如需要采取一些类似于构建器的行动,又应怎样操作呢?通过Java 1.1的实例初始化,我们可以有效地为一个匿名内部类创建一个构建器:
//: Parcel9.java// Using "instance initialization" to perform // construction on an anonymous inner classpackage c07.innerscopes;public class Parcel9 {  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; }    };  }  public static void main(String[] args) {    Parcel9 p = new Parcel9();    Destination d = p.dest("Tanzania", 101.395F);  }} ///:~

在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分执行。所以实际上,一个实例初始化模块就是一个匿名内部类的构建器。当然,它的功能是有限的;我们不能对实例初始化模块进行过载处理,所以只能拥有这些构建器的其中一个。
   5.1 链接到外部类
迄今为止,我们见到的内部类好象仅仅是一种名字隐藏以及代码组织方案。尽管这些功能非常有用,但似乎并不特别引人注目。然而,我们还忽略了另一个重要的事实。创建自己的内部类时,那个类的对象同时拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能访问那个封装对象的成员——毋需取得任何资格。除此以外,内部类拥有对封装类所有元素的访问权限(注释)。下面这个例子阐示了这个问题:
//: Sequence.java// Holds a sequence of Objectsinterface Selector {  boolean end();  Object current();  void next();}public class Sequence {  private Object[] o;  private int next = 0;  public Sequence(int size) {    o = new Object[size];  }  public void add(Object x) {    if(next < o.length) {      o[next] = x;      next++;    }  }  private class SSelector implements Selector {    int i = 0;    public boolean end() {      return i == o.length;    }    public Object current() {      return o[i];    }    public void next() {      if(i < o.length) i++;    }  }  public Selector getSelector() {    return new SSelector();  }  public static void main(String[] args) {    Sequence s = new Sequence(10);    for(int i = 0; i < 10; i++)      s.add(Integer.toString(i));    Selector sl = s.getSelector();        while(!sl.end()) {      System.out.println((String)sl.current());      sl.next();    }  }} ///:~

:这与C++“嵌套类”的设计颇有不同,后者只是一种单纯的名字隐藏机制。在C++中,没有指向一个封装对象的链接,也不存在默认的访问权限。

其中,Sequence只是一个大小固定的对象数组,有一个类将其封装在内部。我们调用add(),以便将一个新对象添加到Sequence末尾(如果还有地方的话)。为了取得Sequence中的每一个对象,要使用一个名为Selector的接口,它使我们能够知道自己是否位于最末尾(end()),能观看当前对象(current() Object),以及能够移至Sequence内的下一个对象(next() Object)。由于Selector是一个接口,所以其他许多类都能用它们自己的方式实现接口,而且许多方法都能将接口作为一个自变量使用,从而创建一般的代码。
在这里,SSelector是一个私有类,它提供了Selector功能。在main()中,大家可看到Sequence的创建过程,在它后面是一系列字串对象的添加。随后,通过对getSelector()的一个调用生成一个Selector。并用它在Sequence中移动,同时选择每一个项目。
从表面看,SSelector似乎只是另一个内部类。但不要被表面现象迷惑。请注意观察end(),current()以及next(),它们每个方法都引用了o。o是个不属于SSelector一部分的句柄,而是位于封装类里的一个private字段。然而,内部类可以从封装类访问方法与字段,就象已经拥有了它们一样。这一特征对我们来说是非常方便的,就象在上面的例子中看到的那样。
因此,我们现在知道一个内部类可以访问封装类的成员。这是如何实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管所有这些细节。但我们现在也可以理解内部类的一个对象只能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的句柄进行初始化。若不能访问那个句柄,编译器就会报错。进行所有这些操作的时候,大多数时候都不要求程序员的任何介入。

 5.2 static内部类
为正确理解static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。然而,假如我们说一个内部类是static的,这种说法却是不成立的。static内部类意味着:
(1) 为创建一个static内部类的对象,我们不需要一个外部类对象。
(2) 不能从static内部类的一个对象中访问一个外部类对象。
但存在一些限制:由于static成员只能位于一个类的外部级别,所以内部类不可拥有static数据或static内部类。
倘若为了创建内部类的对象而不需要创建外部类的一个对象,那么可将所有东西都设为static。为了能正常工作,同时也必须将内部类设为static。如下所示:
//: Parcel10.java// Static inner classespackage c07.parcel10;abstract class Contents {  abstract public int value();}interface Destination {  String readLabel();}public class Parcel10 {  private static class PContents   extends Contents {    private int i = 11;    public int value() { return i; }  }  protected static class PDestination      implements Destination {    private String label;    private PDestination(String whereTo) {      label = whereTo;    }    public String readLabel() { return label; }  }  public static Destination dest(String s) {    return new PDestination(s);  }  public static Contents cont() {    return new PContents();  }  public static void main(String[] args) {    Contents c = cont();    Destination d = dest("Tanzania");  }} ///:~

在main()中,我们不需要Parcel10的对象;相反,我们用常规的语法来选择一个static成员,以便调用将句柄返回Contents和Destination的方法。
通常,我们不在一个接口里设置任何代码,但static内部类可以成为接口的一部分。由于类是“静态”的,所以它不会违反接口的规则——static内部类只位于接口的命名空间内部:
//: IInterface.java// Static inner classes inside interfacesinterface IInterface {  static class Inner {    int i, j, k;    public Inner() {}    void f() {}  }} ///:~

在本书早些时候,我建议大家在每个类里都设置一个main(),将其作为那个类的测试床使用。这样做的一个缺点就是额外代码的数量太多。若不愿如此,可考虑用一个static内部类容纳自己的测试代码。如下所示:
//: TestBed.java// Putting test code in a static inner classclass TestBed {  TestBed() {}  void f() { System.out.println("f()"); }  public static class Tester {    public static void main(String[] args) {      TestBed t = new TestBed();      t.f();    }  }} ///:~

这样便生成一个独立的、名为TestBed$Tester的类(为运行程序,请使用“java TestBed$Tester”命令)。可将这个类用于测试,但不需在自己的最终发行版本中包含它。

  5.3 引用外部类对象
若想生成外部类对象的句柄,就要用一个点号以及一个this来命名外部类。举个例子来说,在Sequence.SSelector类中,它的所有方法都能产生外部类Sequence的存储句柄,方法是采用Sequence.this的形式。结果获得的句柄会自动具备正确的类型(这会在编译期间检查并核实,所以不会出现运行期的开销)。
有些时候,我们想告诉其他某些对象创建它某个内部类的一个对象。为达到这个目的,必须在new表达式中提供指向其他外部类对象的一个句柄,就象下面这样:
//: Parcel11.java// Creating inner classespackage c07.parcel11;public class Parcel11 {  class Contents {    private int i = 11;    public int value() { return i; }  }  class Destination {    private String label;    Destination(String whereTo) {      label = whereTo;    }    String readLabel() { return label; }  }  public static void main(String[] args) {    Parcel11 p = new Parcel11();    // Must use instance of outer class    // to create an instances of the inner class:    Parcel11.Contents c = p.new Contents();    Parcel11.Destination d =      p.new Destination("Tanzania");  }} ///:~

为直接创建内部类的一个对象,不能象大家或许猜想的那样——采用相同的形式,并引用外部类名Parcel11。此时,必须利用外部类的一个对象生成内部类的一个对象:
Parcel11.Contents c = p.new Contents();
因此,除非已拥有外部类的一个对象,否则不可能创建内部类的一个对象。这是由于内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,如果生成一个static内部类,就不需要指向外部类对象的一个句柄。

 5.4 从内部类继承
由于内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候,情况会稍微变得有些复杂。这儿的问题是封装类的“秘密”句柄必须获得初始化,而且在衍生类中不再有一个默认的对象可以连接。解决这个问题的办法是采用一种特殊的语法,明确建立这种关联:
//: InheritInner.java// Inheriting an inner classclass WithInner {  class Inner {}}public class InheritInner     extends WithInner.Inner {  //! InheritInner() {} // Won't compile  InheritInner(WithInner wi) {    wi.super();  }  public static void main(String[] args) {    WithInner wi = new WithInner();    InheritInner ii = new InheritInner(wi);  }} ///:~

从中可以看到,InheritInner只对内部类进行了扩展,没有扩展外部类。但在需要创建一个构建器的时候,默认对象已经没有意义,我们不能只是传递封装对象的一个句柄。此外,必须在构建器中采用下述语法:
enclosingClassHandle.super();
它提供了必要的句柄,以便程序正确编译。

6、为什么要用内部类:控制框架
到目前为止,大家已接触了对内部类的运作进行描述的大量语法与概念。但这些并不能真正说明内部类存在的原因。为什么Sun要如此麻烦地在Java 1.1里添加这样的一种基本语言特性呢?答案就在于我们在这里要学习的“控制框架”。
一个“应用程序框架”是指一个或一系列类,它们专门设计用来解决特定类型的问题。为应用应用程序框架,我们可从一个或多个类继承,并覆盖其中的部分方法。我们在覆盖方法中编写的代码用于定制由那些应用程序框架提供的常规方案,以便解决自己的实际问题。“控制框架”属于应用程序框架的一种特殊类型,受到对事件响应的需要的支配;主要用来响应事件的一个系统叫作“由事件驱动的系统”。在应用程序设计语言中,最重要的问题之一便是“图形用户界面”(GUI),它几乎完全是由事件驱动的。正如大家会在第13章学习的那样,Java 1.1 AWT属于一种控制框架,它通过内部类完美地解决了GUI的问题。
为理解内部类如何简化控制框架的创建与使用,可认为一个控制框架的工作就是在事件“就绪”以后执行它们。尽管“就绪”的意思很多,但在目前这种情况下,我们却是以计算机时钟为基础。随后,请认识到针对控制框架需要控制的东西,框架内并未包含任何特定的信息。首先,它是一个特殊的接口,描述了所有控制事件。它可以是一个抽象类,而非一个实际的接口。由于默认行为是根据时间控制的,所以部分实施细节可能包括:

//: Event.java// The common methods for any control eventpackage c07.controller;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();} ///:~

希望Event(事件)运行的时候,构建器即简单地捕获时间。同时ready()告诉我们何时该运行它。当然,ready()也可以在一个衍生类中被覆盖,将事件建立在除时间以外的其他东西上。
action()是事件就绪后需要调用的方法,而description()提供了与事件有关的文字信息。
下面这个文件包含了实际的控制框架,用于管理和触发事件。第一个类实际只是一个“助手”类,它的职责是容纳Event对象。可用任何适当的集合替换它。
//: Controller.java// Along with Event, the generic// framework for all control systems:package c07.controller;// This is just a way to hold Event objects.class EventSet {  private Event[] events = new Event[100];  private int index = 0;  private int next = 0;  public void add(Event e) {    if(index <= events.length)      return; // (In real life, throw exception)    events[index++] = e;  }  public Event getNext() {    boolean looped = false;    int start = next;    do {      next = (next + 1) % events.length;      // See if it has looped to the beginning:      if(start == next) looped = true;      // If it loops past start, the list       // is empty:      if((next == (start + 1) % events.length)         && looped)        return null;    } while(events[next] == null);    return events[next];  }  public void removeCurrent() {    events[next] = null;  }}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();      }    }  }} ///:~

EventSet可容纳100个事件(若在这里使用来自第8章的一个“真实”集合,就不必担心它的最大尺寸,因为它会根据情况自动改变大小)。index(索引)在这里用于跟踪下一个可用的空间,而next(下一个)帮助我们寻找列表中的下一个事件,了解自己是否已经循环到头。在对getNext()的调用中,这一点是至关重要的,因为一旦运行,Event对象就会从列表中删去(使用removeCurrent())。所以getNext()会在列表中向前移动时遇到“空洞”。
注意removeCurrent()并不只是指示一些标志,指出对象不再使用。相反,它将句柄设为null。这一点是非常重要的,因为假如垃圾收集器发现一个句柄仍在使用,就不会清除对象。若认为自己的句柄可能象现在这样被挂起,那么最好将其设为null,使垃圾收集器能够正常地清除它们。
Controller是进行实际工作的地方。它用一个EventSet容纳自己的Event对象,而且addEvent()允许我们向这个列表加入新事件。但最重要的方法是run()。该方法会在EventSet中遍历,搜索一个准备运行的Event对象——ready()。对于它发现ready()的每一个对象,都会调用action()方法,打印出description(),然后将事件从列表中删去。
注意在迄今为止的所有设计中,我们仍然不能准确地知道一个“事件”要做什么。这正是整个设计的关键;它怎样“将发生变化的东西同没有变化的东西区分开”?或者用我的话来讲,“改变的意图”造成了各类Event对象的不同行动。我们通过创建不同的Event子类,从而表达出不同的行动。
这里正是内部类大显身手的地方。它们允许我们做两件事情:
(1) 在单独一个类里表达一个控制框架应用的全部实施细节,从而完整地封装与那个实施有关的所有东西。内部类用于表达多种不同类型的action(),它们用于解决实际的问题。除此以外,后续的例子使用了private内部类,所以实施细节会完全隐藏起来,可以安全地修改。
(2) 内部类使我们具体的实施变得更加巧妙,因为能方便地访问外部类的任何成员。若不具备这种能力,代码看起来就可能没那么使人舒服,最后不得不寻找其他方法解决。

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

④:由于某些特殊原因,这对我来说是一个经常需要解决的、非常有趣的问题;原来的例子在《C++ Inside & Out》一书里也出现过,但Java提供了一种更令人舒适的解决方案。

作为应用程序框架的一种典型行为,GreenhouseControls类是从Controller继承的:
//: GreenhouseControls.java// This produces a specific application of the// control system, all in a single class. Inner// classes allow you to encapsulate different// functionality for each type of event.package c07.controller;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";    }  }  private class WaterOn extends Event {    public WaterOn(long eventTime) {      super(eventTime);    }    public void action() {      // Put hardware control code here      water = true;    }    public String description() {      return "Greenhouse water is on";    }  }  private class WaterOff extends Event {    public WaterOff(long eventTime) {      super(eventTime);    }    public void action() {      // Put hardware control code here      water = false;    }    public String description() {      return "Greenhouse water is off";    }  }  private class ThermostatNight extends Event {    public ThermostatNight(long eventTime) {      super(eventTime);    }    public void action() {      // Put hardware control code here      thermostat = "Night";    }    public String description() {      return "Thermostat on night setting";    }  }  private class ThermostatDay extends Event {    public ThermostatDay(long eventTime) {      super(eventTime);    }    public void action() {      // Put hardware control code here      thermostat = "Day";    }    public String description() {      return "Thermostat on day setting";    }  }  // An example of an action() that inserts a   // new one of itself into the event list:  private int rings;  private class Bell extends Event {    public Bell(long eventTime) {      super(eventTime);    }    public void action() {      // Ring bell every 2 seconds, rings times:      System.out.println("Bing!");      if(--rings > 0)        addEvent(new Bell(          System.currentTimeMillis() + 2000));    }    public String description() {      return "Ring bell";    }  }  private class Restart extends Event {    public Restart(long eventTime) {      super(eventTime);    }    public void action() {      long tm = System.currentTimeMillis();      // Instead of hard-wiring, you could parse      // configuration information from a text      // file here:      rings = 5;      addEvent(new ThermostatNight(tm));      addEvent(new LightOn(tm + 1000));      addEvent(new LightOff(tm + 2000));      addEvent(new WaterOn(tm + 3000));      addEvent(new WaterOff(tm + 8000));      addEvent(new Bell(tm + 9000));      addEvent(new ThermostatDay(tm + 10000));      // Can even add a Restart object!      addEvent(new Restart(tm + 20000));    }    public String description() {      return "Restarting system";    }  }  public static void main(String[] args) {    GreenhouseControls gc =       new GreenhouseControls();    long tm = System.currentTimeMillis();    gc.addEvent(gc.new Restart(tm));    gc.run();  } } ///:~

注意light(灯光)、water(供水)、thermostat(调温)以及rings都隶属于外部类GreenhouseControls,所以内部类可以毫无阻碍地访问那些字段。此外,大多数action()方法也涉及到某些形式的硬件控制,这通常都要求发出对非Java代码的调用。
大多数Event类看起来都是相似的,但Bell(铃)和Restart(重启)属于特殊情况。Bell会发出响声,若尚未响铃足够的次数,它会在事件列表里添加一个新的Bell对象,所以以后会再度响铃。请注意内部类看起来为什么总是类似于多重继承:Bell拥有Event的所有方法,而且也拥有外部类GreenhouseControls的所有方法。
Restart负责对系统进行初始化,所以会添加所有必要的事件。当然,一种更灵活的做法是避免进行“硬编码”,而是从一个文件里读入它们(第10章的一个练习会要求大家修改这个例子,从而达到这个目标)。由于Restart()仅仅是另一个Event对象,所以也可以在Restart.action()里添加一个Restart对象,使系统能够定期重启。在main()中,我们需要做的全部事情就是创建一个GreenhouseControls对象,并添加一个Restart对象,令其工作起来。
这个例子应该使大家对内部类的价值有一个更加深刻的认识,特别是在一个控制框架里使用它们的时候。此外,在第13章的后半部分,大家还会看到如何巧妙地利用内部类描述一个图形用户界面的行为。完成那里的学习后,对内部类的认识将上升到一个前所未有的新高度。






0 0
原创粉丝点击