9.JAVA编程思想 多形性
欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51040241
“对于面向对象的程序设计语言,多型性是第三种最主要的特征(前两种是数据抽象和继承。”
“多形性”(Polymorphism)从还有一个角度将接口从详细的实施细节中分离出来。亦即实现了“是什么”与“如何做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。不管在项目的创建过程中,还是在须要增加新特性的时候。它们都能够方便地“成长”。
通过合并各种特征与行为。封装技术可创建出新的数据类型。通过对详细实施细节的隐藏,可将接口与实施细节分离,使全部细节成为“private”(私有)。这样的组织方式使那些有程序化编程背景人感觉颇为舒适。
多形性却涉及对“类型”的分解。通过上面的学习,已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这样的能力是十分重要的,由于多个类型(从同样的基础类型中衍生出来)可被当作同一种类型对待。并且仅仅需一段代码,就可以对全部不同的类型进行同样的处理。
利用具有多形性的方法调用。一种类型可将自己与还有一种相似的类型区分开,仅仅要它们都是从同样的基础类型中衍生出来的。
这样的区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。
由浅入深地学习有关多形性的问题(也叫作动态绑定、推迟绑定或者执行期绑定)。
1 上溯造型
可将一个对象作为它自己的类型使用,或者作为它的基础类型的一个对象使用。取得
一个对象句柄,并将其作为基础类型句柄使用的行为就叫作“上溯造型”——由于继承树的画法是基础类位于最上方。
但这样做也会遇到一个问题,例如以下例所看到的:
package com.toad7;
class Note {
privateintvalue;
private Note(intval)
{
value =val;
}
publicstaticfinal
Note middleC =new Note(0),cSharp
=new Note(1),
cFlat =new Note(2);
} // Etc.
class Instrument {
publicvoid play(Noten)
{
System.out.println("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the sameinterface:
class Windextends Instrument
{
// Redefine interfacemethod:
publicvoid play(Noten)
{
System.out.println("Wind.play()");
}
}
publicclass Music {
publicstaticvoid
tune(Instrument i) {
// ...
i.play(Note.middleC);
}
publicstaticvoid
main(String[] args) {
Windflute =new
Wind();
tune(flute);//
Upcasting
}
} // /:~
输出:
Wind.play()
方法Music.tune()接收一个Instrument句柄,同一时候也接收从Instrument衍生出来的全部东西。当一个Wind句柄传递给 tune()的时候。就会出现这样的情况。此时没有造型的必要。
这样做是能够接受的。
Instrument里的接口必须存在于Wind 中,由于Wind是从Instrument 里继承得到的。从 Wind向Instrument的上溯造型可能“缩小”那个接口。但不可能把它变得比 Instrument的完整接口还要小。
1.1 为什么要上溯造型
为什么全部人都应该有意忘记一个对象的类型呢?进行上溯造型时,就可能产生这方面的疑惑。
并且假设让tune()简单地取得一个Wind 句柄,将其作为自己的自变量使用,似乎会更加简单、直观得多。但要注意:假如那样做。就需为系统内Instrument 的每种类型写一个全新的tune()。
示比例如以下:
package com.toad7;
class Note {
privateintvalue;
private Note(intval)
{
value =val;
}
publicstaticfinal
Note middleC =new Note(0),cSharp
=new Note(1),
cFlat =new Note(2);
} // Etc.
class Instrument {
publicvoid play(Noten)
{
System.out.println("Instrument.play()");
}
}
// Wind objects are instruments
// because they have the same interface:
class Windextends Instrument
{
// Redefine interfacemethod:
// publicvoid play(Note n) {
// System.out.println("Wind.play()");
// }
}
publicclass Music {
publicstaticvoid
tune(Instrument i) {
// ...
i.play(Note.middleC);
}
publicstaticvoid
main(String[] args) {
Windflute =new
Wind();
tune(flute);//
Upcasting
}
} // /:~
这样做行得通,但却存在一个极大的弊端:必须为每种新增的Instrument2类编写与类紧密相关的方法。
这意味着第一次就要求多得多的编程量。
以后。假如想加入一个象tune()那样的新方法或者为Instrument加入一个新类型,仍然须要进行大量编码工作。此外,即使忘记对自己的某个方法进行过载设置,编译器也不会提示不论什么错误。
这样一来。类型的整个操作过程就显得极难管理。有失控的危急。
但假如仅仅写一个方法,将基础类作为自变量或參数使用,而不是使用那些特定的衍生类。岂不是会简单得多?也就是说,假设我们能不顾衍生类,仅仅让自己的代码与基础类打交道,那么省下的工作量将是难以预计的。
这正是“多形性”大显身手的地方。
2 深入理解
2.1 方法调用的绑定
将一个方法调用同一个方法主体连接到一起就称为“绑定”(Binding)。
若在程序执行曾经执行绑定(由编译器和链接程序。假设有的话),就叫作“早期绑定”。大家曾经也许从未听说过这个术语,由于它在不论什么程序化语言里都是不可能的。C 编译器仅仅有一种方法调用。那就是“早期绑定”。
上述程序最令人迷惑不解的地方全与早期绑定有关。由于在仅仅有一个Instrument句柄的前提下,编译器不知道详细该调用哪个方法。
解决办法就是“后期绑定”,它意味着绑定在执行期间进行。以对象的类型为基础。后期绑定也叫作“动态绑定”或“执行期绑定”。若一种语言实现了后期绑定,同一时候必须提供一些机制,可在执行期间推断对象的类型。并分别调用适当的方法。也就是说,编译器此时依旧不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所差别的。
但我们至少能够这样觉得:它们都要在对象中安插某些特殊类型的信息。
Java 中绑定的全部方法都採用后期绑定技术,除非一个方法已被声明成final。这意味着我们通常不必决定是否应进行后期绑定——它是自己主动发生的。
把一个方法声明成final能防止其它人覆盖那个方法。但或许更重要的一点是,它可有效地“关闭”动态绑定,或者告诉编译器不须要进行动态绑定。这样一来,编译器就可为final 方法调用生成效率更高的代码。
2.2 产生正确的行为
Java 里绑定的全部方法都通过后期绑定具有多形性以后,就能够对应地编写自己的代码。令其与基础类沟通。此时。全部的衍生类都保证能用同样的代码正常地工作。或者换用还有一种方法,我们能够“将一条消息发给一个对象,让对象自行推断要做什么事情。”
在面向对象的程序设计中,有一个经典的“形状”样例。因为它非常easy用可视化的形式表现出来。所以常常都用它说明问题。但非常不幸的是,它可能误导刚開始学习的人觉得 OOP仅仅是为图形化编程设计的,这样的认识当然是错误的。
形状样例有一个基础类,名为Shape;另外还有大量衍生类型:Circle(圆形)。Square(方形)。Triangle(三角形)等等。大家之所以喜欢这个样例。由于非常easy理解“圆属于形状的一种类型”等概念。
以下这幅继承图向我们展示了它们的关系:
上溯造型可用以下这个语句简单地表现出来:
Shape s = new Circle();
我们创建了Circle 对象。并将结果句柄马上赋给一个Shape。这表面看起来似乎属于错误操作(将一种类型分配给还有一个)。但实际是全然可行的——由于依照继承关系。Circle属于Shape 的一种。因此编译器认可上述语句。不会向我们提示一条出错消息。
当我们调用当中一个基础类方法时(已在衍生类里覆盖):
s.draw();
相同地。大家或许觉得会调用Shape的draw()。由于这毕竟是一个Shape句柄。
那么编译器如何才干知道该做其它不论什么事情呢?但此时实际调用的是Circle.draw(),由于后期绑定已经介入(多形性)。
示比例如以下:
package com.toad7;
class Shape {
void draw() {
}
void erase() {
}
}
class Circleextends
Shape {
void draw() {
System.out.println("Circle.draw()");
}
void erase() {
System.out.println("Circle.erase()");
}
}
class Squareextends
Shape {
void draw() {
System.out.println("Square.draw()");
}
void erase() {
System.out.println("Square.erase()");
}
}
class Triangleextends
Shape {
void draw() {
System.out.println("Triangle.draw()");
}
void erase() {
System.out.println("Triangle.erase()");
}
}
publicclass Shapes {
publicstatic Shape randShape() {
switch ((int)
(Math.random() * 3)) {
default:// To quiet the compiler
case 0:
returnnew Circle();
case 1:
returnnew Square();
case 2:
returnnew Triangle();
}
}
publicstaticvoid
main(String[] args) {
Shape[]s =new
Shape[9];
// Fill up the arraywith shapes:
for (inti
= 0; i <s.length;i++)
s[i] =randShape();
// Makepolymorphicmethod calls:
for (inti
= 0; i <s.length;i++)
s[i].draw();
}
} // /:~
输出:
Circle.draw()
Triangle.draw()
Circle.draw()
Circle.draw()
Circle.draw()
Square.draw()
Triangle.draw()
Square.draw()
Square.draw()
针对从Shape 衍生出来的全部东西,Shape 建立了一个通用接口——也就是说。全部(几何)形状都能够描绘和删除。衍生类覆盖了这些定义,为每种特殊类型的几何形状都提供了独一无二的行为。
在主类Shapes 里。包括了一个static 方法。名为 randShape()。它的作用是在每次调用它时为某个随机选择的Shape 对象生成一个句柄。请注意上溯造型是在每一个return 语句里发生的。这个语句取得指向一个Circle。Square 或者Triangle 的句柄,并将其作为返回类型 Shape发给方法。所以不管什么时候调用这种方法,就绝对没机会了解它的详细类型究竟是什么。由于肯定会获得一个单纯的Shape 句柄。
main()包括了 Shape 句柄的一个数组。当中的数据通过对randShape()的调用填入。
在这个时候。我们知道自己拥有Shape,但不知除此之外不论什么详细的情况(编译器相同不知)。然而,当我们在这个数组里步进,
并为每一个元素调用draw()的时候。与各类型有关的正确行为会魔术般地发生。
因为几何形状是每次随机选择的,所以每次执行都可能有不同的结果。
之所以要突出形状的随机选择,是为了体会一点:为了在编译的时候发出正确的调用。编译器毋需获得不论什么特殊的情报。对draw()的全部调用都是通过动态绑定进行的。
2.3 扩展性
让我们仍然返回乐器(Instrument)演示样例。由于存在多形性,所以可依据自己的须要向系统里增加随意多的新类型,同一时候毋需更改true()方法。在一个设计良好的OOP程序中,我们的大多数或者全部方法都会遵从tune()的模型,并且仅仅与基础类接口通信。我们说这种程序具有“扩展性”,由于能够从通用的基础类继承新的数据类型。从而新添一些功能。
假设是为了适应新类的要求,那么对基础类接口进行操纵的方法根本不须要改变,
对于乐器样例,如果我们在基础类里增加很多其它的方法。以及一系列新类。那么会出现什么情况呢?以下是示意图:
全部这些新类都能与老类——tune()默契地工作,毋需对tune()作不论什么调整。
即使 tune()位于一个独立的文件中。而将新方法加入到Instrument 的接口。tune()也能正确地工作。不须要又一次编译。以下这个程序是对上述示意图的详细实现:
package com.toad7;
importjava.util.*;
class Instrument3 {
publicvoid play() {
System.out.println("Instrument3.play()");
}
public String what() {
return"Instrument3";
}
publicvoid adjust() {
}
}
class Wind3extends Instrument3
{
publicvoid play() {
System.out.println("Wind3.play()");
}
public String what() {
return"Wind3";
}
publicvoid adjust() {
}
}
class Percussion3extends
Instrument3 {
publicvoid play() {
System.out.println("Percussion3.play()");
}
public String what() {
return"Percussion3";
}
publicvoid adjust() {
}
}
class Stringed3extends
Instrument3 {
publicvoid play() {
System.out.println("Stringed3.play()");
}
public String what() {
return"Stringed3";
}
publicvoid adjust() {
}
}
class Brass3extends
Wind3 {
publicvoid play() {
System.out.println("Brass3.play()");
}
publicvoid adjust() {
System.out.println("Brass3.adjust()");
}
}
class Woodwind3extends
Wind3 {
publicvoid play() {
System.out.println("Woodwind3.play()");
}
public String what() {
return"Woodwind3";
}
}
publicclass Music3 {
// Doesn't care abouttype, so new types
// added to thesystem still work right:
staticvoid tune(Instrument3i)
{
// ...
i.play();
}
staticvoid tuneAll(Instrument3[]e)
{
for (inti
= 0; i <e.length;i++)
tune(e[i]);
}
publicstaticvoid
main(String[] args) {
Instrument3[]orchestra =new
Instrument3[5];
inti = 0;
//Upcastingduring addition to the array:
orchestra[i++]
= new Wind3();
orchestra[i++]
= new Percussion3();
orchestra[i++]
= new Stringed3();
orchestra[i++]
= new Brass3();
orchestra[i++]
= new Woodwind3();
tuneAll(orchestra);
}
} // /:~
新方法是what()和adjust()。前者返回一个String句柄,同一时候返回对那个类的说明;后者使我们能对每种乐器进行调整。
在main()中,当我们将某样东西置入Instrument3数组时,就会自己主动上溯造型到 Instrument3。
能够看到。在环绕tune()方法的其它全部代码都发生变化的同一时候,tune()方法却丝毫不受它们的影响,依旧故我地正常工作。这正是利用多形性希望达到的目标。我们对代码进行改动后。不会对程序中不应受到影响的部分造成影响。此外。我们觉得多形性是一种至关重要的技术,它同意程序猿“将发生改变的东西同没有发生改变的东西区分开”。
3 覆盖与过载
在以下这个程序中。方法play()的接口会在被覆盖的过程中发生变化。这意味着我们实际并没有“覆盖”方法,而是使其“过载”。编译器同意我们对方法进行过载处理,使其不报告出错。
但这样的行为可能并非我们所希望的。
以下是个样例:
package com.toad7;
class NoteX {
publicstaticfinalint
MIDDLE_C = 0,C_SHARP
= 1,C_FLAT = 2;
}
class InstrumentX {
publicvoid play(intNoteX)
{
System.out.println("InstrumentX.play()");
}
}
class WindXextends InstrumentX
{
//OOPS! Changes the method interface:
publicvoid play(NoteXn)
{
System.out.println("WindX.play(NoteXn)");
}
}
publicclass WindError {
publicstaticvoid
tune(InstrumentX i) {
// ...
i.play(NoteX.MIDDLE_C);
}
publicstaticvoid
main(String[] args) {
WindX flute =new
WindX();
tune(flute);// Not the desiredbehavior!
}
} ///:~
输出是:
InstrumentX.play()
在InstrumentX 中。play()方法採用了一个int(整数)数值,它的标识符是NoteX。
也就是说,即使 NoteX 是一个类名。也能够把它作为一个标识符使用。编译器不会报告出错。
但在WindX中,play()採用一个NoteX 句柄。它有一个标识符 n。
即便我们使用“play(NoteX
NoteX)”,编译器也不会报告错误。这样一来。看起来就象是程序猿有意覆盖play()的功能,但对方法的类型定义却略微有些不确切。然而,编译器此时假定的是程序猿有意进行“过载”,而非“覆盖”。请细致体会这两个术语的差别。
“过载”是指同一样东西在不同的地方具有多种含义。而“覆盖”是指它随时随地都仅仅有一种含义,仅仅是原先的含义全然被后来的含义代替了。
请注意假设遵守标准的Java 命名规范,自变量标识符就应该是noteX,这样可把它与类名区分开。 在tune 中,“InstrumentXi”会发出play()消息,同一时候将某个 NoteX 成员作为自变量使用(MIDDLE_C)。
因为NoteX 包括了int 定义。过载的play()方法的int 版本号会得到调用。同一时候因为它尚未被“覆盖”,所以会使用基础类版本号。
4 抽象类和方法
在全部乐器(Instrument)样例中。基础类 Instrument 内的方法都肯定是“伪”方法。
若去调用这些方法。就会出现错误。那是因为Instrument的意图是为从它衍生出去的全部类都创建一个通用接口。
之所以要建立这个通用接口,唯一的原因就是它能为不同的子类型作出不同的表示。
它为我们建立了一种基本形式,使我们能定义在全部衍生类里“通用”的一些东西。
为阐述这个观念,还有一个方法是把 Instrument称为“抽象基础类”(简称“抽象类”)。若想通过该通用接口处理一系列类,就须要创建一个抽象类。
对全部与基础类声明的签名相符的衍生类方法。都能够通过动态绑定机制进行调用(然而。正如之前描写叙述的那样,假设方法名与基础类同样,但自变量或參数不同,就会出现过载现象,那也许并不是我们所愿意的)。
假设有一个象Instrument 那样的抽象类,那个类的对象差点儿肯定没有什么意义。
换言之,Instrument的作用不过表达接口,而不是表达一些详细的实施细节。所以创建一个Instrument对象是没有意义的。并且我们通常都应禁止用户那样做。
为达到这个目的。可令Instrument 内的全部方法都显示出错消息。
但这样做会延迟信息到执行期,并要求在用户那一面进行彻底、可靠的測试。不管怎样,最好的方法都是在编译期间捕捉到问题。
针对这个问题,Java 专门提供了一种机制,名为“抽象方法”。它属于一种不完整的方法,仅仅含有一个声明,没有方法主体。
以下是抽象方法声明时採用的语法:
abstract void X();
包括了抽象方法的一个类叫作“抽象类”。假设一个类里包括了一个或多个抽象方法,类就必须指定成abstract(抽象)。
否则,编译器会向我们报告一条出错消息。
若一个抽象类是不完整的,那么一旦有人试图生成那个类的一个对象,编译器又会採取什么行动呢?因为不
能安全地为一个抽象类创建属于它的对象。所以会从编译器那里获得一条出错提示。
通过这样的方法,编译器可保证抽象类的“清纯性”,我们不必操心会误用它。
假设从一个抽象类继承,并且想生成新类型的一个对象,就必须为基础类中的全部抽象方法提供方法定义。假设不这样做(全然能够选择不做)。则衍生类也会是抽象的,并且编译器会强迫我们用abstractkeyword标志那个类的“抽象”本质。
即使不包含不论什么abstract 方法,亦可将一个类声明成“抽象类”。
假设一个类不是必需拥有不论什么抽象方法,并且我们想禁止那个类的全部实例,这样的能力就会显得很实用。
Instrument类可非常轻松地转换成一个抽象类。仅仅有当中一部分方法会变成抽象方法,由于使一个类抽象以后,并不会强迫我们将它的全部方法都同一时候变成抽象。
以下是它看起来的样子:
代码例如以下:
package com.toad7;
importjava.util.*;
abstractclass Instrument4 {
inti;//
storage allocated for each
publicabstractvoid
play();
public String what() {
return"Instrument4";
}
publicabstractvoid
adjust();
}
class Wind4extends Instrument4
{
publicvoid play() {
System.out.println("Wind4.play()");
}
public String what() {
return"Wind4";
}
publicvoid adjust() {
}
}
class Percussion4extends
Instrument4 {
publicvoid play() {
System.out.println("Percussion4.play()");
}
public String what() {
return"Percussion4";
}
publicvoid adjust() {
}
}
class Stringed4extends
Instrument4 {
publicvoid play() {
System.out.println("Stringed4.play()");
}
public String what() {
return"Stringed4";
}
publicvoid adjust() {
}
}
class Brass4extends
Wind4 {
publicvoid play() {
System.out.println("Brass4.play()");
}
publicvoid adjust() {
System.out.println("Brass4.adjust()");
}
}
class Woodwind4extends
Wind4 {
publicvoid play() {
System.out.println("Woodwind4.play()");
}
public String what() {
return"Woodwind4";
}
}
publicclass Music4 {
// Doesn't care abouttype, so new types
// added to thesystem still work right:
staticvoid tune(Instrument4i)
{
// ...
i.play();
}
staticvoid tuneAll(Instrument4[]e)
{
for (inti
= 0; i <e.length;i++)
tune(e[i]);
}
publicstaticvoid
main(String[] args) {
Instrument4[]orchestra =new
Instrument4[5];
inti = 0;
//Upcastingduring addition to the array:
orchestra[i++]
= new Wind4();
orchestra[i++]
= new Percussion4();
orchestra[i++]
= new Stringed4();
orchestra[i++]
= new Brass4();
orchestra[i++]
= new Woodwind4();
tuneAll(orchestra);
}
} // /:~
输出例如以下:
Wind4.play()
Percussion4.play()
Stringed4.play()
Brass4.play()
Woodwind4.play()
除基础类以外。实际并没有进行什么改变。创建抽象类和方法有时对我们很实用,由于它们使一个类的抽象变成明显的事实,可明白告诉用户和编译器自己打算怎样用它
5 接口
“interface”(接口)keyword使抽象的概念更深入了一层。
我们可将其想象为一个“纯”抽象类。它同意创建者规定一个类的基本形式:方法名、自变量列表以及返回类型。但不规定方法主体。接口也包括了基本数据类型的数据成员,但它们都默觉得static 和final。
接口仅仅提供一种形式。并不提供实施的细节。
接口这样描写叙述自己:“对于实现我的全部类,看起来都应该象我如今这个样子”。
因此,採用了一个特定接口的全部代码都知道对于那个接口可能会调用什么方法。这便是接口的全部含义。所以我们常把接口用于建立类和类之间的一个“协议”。有些面向对象的程序设计语言採用了一个名为“protocol”(协议)的keyword,它做的便是与接口同样的事情。
为创建一个接口,请使用interfacekeyword,而不要用 class。与类相似。我们可在 interfacekeyword的前面添加一个 publickeyword(但仅仅有接口定义于同名的一个文件内);或者将其省略,营造一种“友好的”状态。
为了生成与一个特定的接口(或一组接口)相符的类,要使用implements(实现)keyword。
我们要表达的意思是“接口看起来就象那个样子,这儿是它详细的工作细节”。
除这些之外,我们其它的工作都与继承极为相似。以下是乐器样例的示意图:
详细实现了一个接口以后,就获得了一个普通的类,可用标准方式对其进行扩展。
可决定将一个接口中的方法声明明白定义为“public”。但即便不明白定义。它们也会默觉得 public。所以在实现一个接口的时候。来自接口的方法必须定义成public。否则的话,它们会默觉得“友好的”。并且会限制我们在继承过程中对一个方法的訪问——Java 编译器不同意我们那样做。
在Instrument 样例的改动版本号中,大家可明白地看出这一点。
注意接口中的每一个方法都严格地是一个声明,它是编译器唯一同意的。除此以外。Instrument5 中没有一个方法被声明为public,但它们都会自己主动获得public属性。
演示样例:
package com.toad7;
importjava.util.*;
interface Instrument5 {
// Compile-timeconstant:
inti = 5;//
static & final
// Cannot have methoddefinitions:
void play();// Automaticallypublic
Stringwhat();
void adjust();
}
class Wind5implements
Instrument5 {
publicvoid play() {
System.out.println("Wind5.play()");
}
public String what() {
return"Wind5";
}
publicvoid adjust() {
}
}
class Percussion5implements
Instrument5 {
publicvoid play() {
System.out.println("Percussion5.play()");
}
public String what() {
return"Percussion5";
}
publicvoid adjust() {
}
}
class Stringed5implements
Instrument5 {
publicvoid play() {
System.out.println("Stringed5.play()");
}
public String what() {
return"Stringed5";
}
publicvoid adjust() {
}
}
class Brass5extends
Wind5 {
publicvoid play() {
System.out.println("Brass5.play()");
}
publicvoid adjust() {
System.out.println("Brass5.adjust()");
}
}
class Woodwind5extends
Wind5 {
publicvoid play() {
System.out.println("Woodwind5.play()");
}
public String what() {
return"Woodwind5";
}
}
publicclass Music5 {
staticvoid tune(Instrument5i)
{
// ...
i.play();
}
staticvoid tuneAll(Instrument5[]e)
{
for (inti
= 0; i <e.length;i++)
tune(e[i]);
}
publicstaticvoid
main(String[] args) {
Instrument5[]orchestra =new
Instrument5[5];
inti = 0;
//Upcastingduring addition to the array:
orchestra[i++]
= new Wind5();
orchestra[i++]
= new Percussion5();
orchestra[i++]
= new Stringed5();
orchestra[i++]
= new Brass5();
orchestra[i++]
= new Woodwind5();
tuneAll(orchestra);
}
} // /:~
输出例如以下:
Wind5.play()
Percussion5.play()
Stringed5.play()
Brass5.play()
Woodwind5.play()
代码剩余的部分按同样的方式工作。
我们能够自由决定上溯造型到一个名为Instrument5的“普通”类,一个名为Instrument5的“抽象”类。或者一个名为Instrument5的“接口”。
全部行为都是同样的。其实,我们在 tune()方法中能够发现没有不论什么证据显示 Instrument5 究竟是个“普通”类、“抽象”类还是一个“接口”。
这是做是有益的:每种方法都使程序猿能对对象的创建与使用进行不同的控制。
5.1 Java 的“多重继承”
接口仅仅是比抽象类“更纯”的一种形式。
它的用途并不止那些。
由于接口根本没有详细的实施细节——也就是说,没有与存储空间与“接口”关联在一起——所以没有不论什么办法能够防止多个接口合并到一起。
这一点是至关重要的,由于我们常常都须要表达这样一个意思:“x 从属于 a,也从属于 b。也从属于 c”。在C++中,将多个类合并到一起的行动称作“多重继承”,并且操作较为不便。由于每一个类都可能有一套自己的实施细节。
在
Java 中。我们可採取相同的行动。但仅仅有当中一个类拥有详细的实施细节。
所以在合并多个接口的时候。C++的问题不会在Java 中重演。
例如以下所看到的:
在一个衍生类中。我们并不一定要拥有一个抽象或详细(没有抽象方法)的基础类。假设确实想从一个非接口继承。那么仅仅能从一个继承。
剩余的全部基本元素都必须是“接口”。
我们将全部接口名置于 implementskeyword的后面,并用逗号分隔它们。可依据须要使用多个接口。并且每一个接口都会成为一个独立的类型。可对其进行上溯造型。以下这个样例展示了一个“详细”类同几个接口合并的情况,它终于生成了一个新类:
示比例如以下:
package com.toad7;
importjava.util.*;
interface CanFight {
void fight();
}
interface CanSwim {
void swim();
}
interface CanFly {
void fly();
}
class ActionCharacter {
publicvoid fight() {
}
}
class Heroextends ActionCharacterimplements
CanFight, CanSwim, CanFly {
publicvoid swim() {
System.out.println("can
swim");
}
publicvoid fly() {
System.out.println("can
fly");
}
}
publicclass Adventure {
staticvoid t(CanFightx)
{
x.fight();
}
staticvoid u(CanSwimx)
{
x.swim();
}
staticvoid v(CanFlyx)
{
x.fly();
}
staticvoid w(ActionCharacterx)
{
x.fight();
}
publicstaticvoid
main(String[] args) {
Heroi =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 设计的,所以这样做不会有不论什么问题。并且程序猿不必对此加以不论什么特别的关注。
注意上述样例已揭示接口最关键的作用,也是使用接口最重要的一个原因:能上溯造型至多个基础类。使用接口的第二个原因与使用抽象基础类的原因是一样的:防止客户程序猿制作这个类的一个对象。以及规定它不过一个接口。
这样便带来了一个问题:究竟应该使用一个接口还是一个抽象类呢?若使用接口。我们能够同一时候获得抽象类以及接口的优点。所以假如想创建的基础类没有不论什么方法定义或者成员变量,那么不管怎样都愿意使用接口。而不要选择抽象类。
其实,假设事先知道某种东西会成为基础类,那么第一个选择就是把它变成一个接口。唯独在必须用法定义或者成员变量的时候,才应考虑採用抽象类。
5.2 通过继承扩展接口
利用继承技术,可方便地为一个接口加入新的方法声明,也能够将几个接口合并成一个新接口。
在这两种情况下。终于得到的都是一个新接口。例如以下例所看到的:
package com.toad7;
//: HorrorShow.java
// Extending an interface withinheritance
interface Monster {
void menace();
}
interface DangerousMonsterextends
Monster {
void destroy();
}
interface Lethal {
void kill();
}
class DragonZillaimplements
DangerousMonster {
publicvoid menace() {
System.out.println("DragonZilla.menace");
}
publicvoid destroy() {
System.out.println("DragonZilla.destroy");
}
}
interface Vampireextends
DangerousMonster, Lethal {
void drinkBlood();
}
class HorrorShow {
staticvoid u(Monsterb)
{
b.menace();
}
staticvoid v(DangerousMonsterd)
{
d.menace();
d.destroy();
}
publicstaticvoid
main(String[] args) {
DragonZillaif2 =new
DragonZilla();
u(if2);
v(if2);
}
} // /:~
DangerousMonster是对Monster 的一个简单的扩展,终于生成了一个新接口。
这是在DragonZilla 里实现的。
Vampire的语法仅在继承接口时才可使用。通常。我们仅仅能对单独一个类应用 extends(扩展)keyword。但因为接口可能由多个其它接口构成。所以在构建一个新接口时。extends可能引用多个基础接口。正如大家看到的那样,接口的名字仅仅是简单地使用逗号分隔。
5.3 常数分组
因为置入一个接口的全部字段都自己主动具有static和final
属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum很相似的效果。
例如以下例所看到的:
publicinterface 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 属性。所以不是必需专门指定。
如今,通过导入,我们能够从包的外部使用常数——就象对其它不论什么包进行的操作那样。此外。也能够用类似Months.JANUARY 的表达式对值进行引用。
当然,我们获得的仅仅是一个int。所以不象C++的enum那样拥有额外的类型安全性。但与将数字强行编码(硬编码)到自己的程序中相比。这样的(经常使用的)技术无疑已经是一个巨大的进步。
我们通常把“硬编码”数字的行为称为“魔术数字”。它产生的代码是很难以维护的。
如确实不想放弃额外的类型安全性,可构建象以下这种一个类:
package com.toad7;
publicfinalclass Month2
{
private Stringname;
private Month2(Stringnm)
{
name =nm;
}
public String toString() {
returnname;
}
publicfinalstatic
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");
publicfinalstatic
Month2[] month = {JAN,JAN,FEB,MAR,APR,MAY,JUN,
JUL,AUG,SEP,OCT,NOV,DEC
};
publicstaticvoid
main(String[] args) {
Month2m = 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));
}
} // /:~
输出例如以下:
January
December
true
true
这个类叫作 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()尾部展示的那样。
5.4 初始化接口中的字段
接口中定义的字段会自己主动具有static 和final 属性。它们不能是“空白final”,但可初始化成很数表达式。
比如:
importjava.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的,所以它们会在首次装载类之后、以及首次訪问不论什么字段之前获得初始化。
以下是一个简单的測试:
publicclass 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);
}
}///:~
合在一起例如以下:
package com.toad7;
importjava.util.*;
interface RandVals {
intrint = (int)(Math.random()
* 10);
longrlong = (long)(Math.random()
* 10);
floatrfloat = (float)(Math.random()
* 10);
doublerdouble = Math.random()
* 10;
} ///:~
publicclass TestRandVals {
publicstaticvoid
main(String[] args) {
System.out.println(RandVals.rint);
System.out.println(RandVals.rlong);
System.out.println(RandVals.rfloat);
System.out.println(RandVals.rdouble);
}
} ///:~
当然,字段并非接口的一部分,而是保存于那个接口的 static存储区域中。
6 内部类
在Java 1.1 中。可将一个类定义置入还有一个类定义中。这就叫作“内部类”。内部类对我们很实用,由于利用它可对那些逻辑上相互联系的类进行分组,并可控制一个类在还有一个类里的“可见性”。然而。我们必须认识到内部类与曾经讲述的“合成”方法存在着根本的差别。
通常,对内部类的须要并非特别明显的。至少不会马上感觉到自己须要使用内部类。
介绍完内部类的全部语法之后。大家会发现一个特别的样例。通过它应该能够清晰地认识到内部类的优点。
创建内部类的过程是平淡无奇的:将类定义置入一个用于封装它的类内部。
例如以下:
package com.toad7;
publicclass Parcel1 {
class Contents {
privateint
i = 11;
publicint value() {
returni; }
}
class Destination {
private Stringlabel;
Destination(String
whereTo) {
label =
whereTo;
}
String readLabel() { returnlabel; }
}
// Using innerclasses looks just like
// using anyother class, within Parcel1:
publicvoidship(String
dest){
Contents c =new Contents();
Destination d =new Destination(dest);
}
publicstaticvoidmain(String[]args){
Parcel1 p =new Parcel1();
p.ship("Tanzania");
}
}///:~
若在ship()内部使用。内部类的使用看起来和其它不论什么类都没什么分别。在这里,唯一明显的差别就是它的名字嵌套在 Parcel1里面。但大家不久就会知道,这事实上并不是唯一的差别。
更典型的一种情况是。一个外部类拥有一个特殊的方法,它会返回指向一个内部类的句柄。就象以下这样:
package com.toad7;
publicclass Parcel2 {
class Contents {
privateinti
= 11;
publicint value() {
returni;
}
}
class Destination {
private Stringlabel;
Destination(StringwhereTo) {
label =whereTo;
}
StringreadLabel() {
returnlabel;
}
}
public Destination to(Strings)
{
returnnew Destination(s);
}
public Contents cont() {
returnnew Contents();
}
publicvoid ship(Stringdest)
{
Contentsc = cont();
Destinationd = to(dest);
}
publicstaticvoid
main(String[] args) {
Parcel2p =new
Parcel2();
p.ship("Tanzania");
Parcel2q =new
Parcel2();
// Defining handlesto inner classes:
Parcel2.Contentsc =q.cont();
Parcel2.Destinationd =q.to("Borneo");
System.out.println(d.readLabel());
}
} // /:~
若想在除外部类非static 方法内部之外的不论什么地方生成内部类的一个对象。必须将那个对象的类型设为“外部类名.内部类名”,就象main()中展示的那样。
6.1 内部类和上溯造型
内部类看起来仍然没什么特别的地方。
毕竟,用它实现隐藏显得有些大题小做。
Java已经有一个很优秀的隐藏机制——仅仅同意类成为“友好的”(仅仅在一个包内可见)。而不是把它创建成一个内部类。然而,当我们准备上溯造型到一个基础类(特别是到一个接口)的时候。内部类就開始发挥其关键作用(从用于实现的对象生成一个接口句柄具有与上溯造型至一个基础类同样的效果)。这是因为内部类随后可全然进入不可见或不可用状态——对不论什么人都将如此。
所以我们能够很方便地隐藏实施细节。我们得到的所有回报就是一个基础类或者接口的句柄。并且甚至有可能不知道准确的类型。
就象以下这样:
package com.toad7;
abstractclass Contents {
abstractpublicint
value();
}
interface Destination {
StringreadLabel();
}
publicclass Parcel3 {
privateclass PContentsextends
Contents {
privateinti
= 11;
publicint value() {
returni;
}
}
protectedclass PDestinationimplements
Destination {
private Stringlabel;
private PDestination(StringwhereTo)
{
label =whereTo;
}
public String readLabel() {
returnlabel;
}
}
public Destination dest(Strings)
{
returnnew PDestination(s);
}
public Contents cont() {
returnnew PContents();
}
}
class Test {
publicstaticvoid
main(String[] args) {
Parcel3p =new
Parcel3();
Contentsc =p.cont();
Destinationd =p.dest("Tanzania");
// Illegal -- can'taccess private class:
// !Parcel3.PContents c = p.new PContents();
}
} // /:~
将 Test类,放置到同一个包以下的 Test.java中。执行Test.java就可以。
如今,Contents 和Destination 代表可由客户程序猿使用的接口(记住接口会将自己的全部成员都变成public属性)。
为方便起见,它们置于单独一个文件中,但原始的Contents 和Destination 在它们自己的文件中是相互public 的。
在Parcel3 中,一些新东西已经增加:内部类PContents 被设为 private,所以除了Parcel3 之外,其它不论什么东西都不能訪问它。PDestination被设为 protected。所以除了 Parcel3。Parcel3包内的类(由于protected 也为包赋予了訪问权。也就是说,protected 也是“友好的”),以及Parcel3的继承者之外,其它不论什么东西都不能訪问 PDestination。这意味着客户程序猿对这些成员的认识与訪问将会受到限制。其实。我们甚至不能下溯造型到一个
private内部类(或者一个 protected 内部类,除非自己本身便是一个继承者),由于我们不能訪问名字。就象在classTest 里看到的那样。所以,利用private 内部类。类设计人员可全然禁止其它人依赖类型编码。并可将详细的实施细节全然隐藏起来。
除此以外,从客户程序猿的角度来看,一个接口的范围没有意义的。由于他们不能訪问不属于公共接口类的不论什么额外方法。
这样一来。Java编译器也有机会生成效率更高的代码。
普通(非内部)类不可设为private或protected——仅仅同意 public或者“友好的”。
注意Contents 不必成为一个抽象类。在这儿也能够使用一个普通类,但这样的设计最典型的起点依旧是一个“接口”。
6.2 方法和作用域中的内部类
至此。我们已基本理解了内部类的典型用途。对那些涉及内部类的代码,通常表达的都是“单纯”的内部类。很easy,且极易理解。然而。内部类的设计很全面,不可避免地会遇到它们的其它大量使用方法——假若我们在一个方法甚至一个随意的作用域内创建内部类。有双方面的原因促使我们这样做:
(1)准备实现某种形式的接口,使自己能创建和返回一个句柄。
(2)要解决一个复杂的问题,并希望创建一个类,用来辅助自己的程序方案。
同一时候不愿意把它公开。
在以下这个样例里,将改动前面的代码,以便使用:
(1) 在一个方法内定义的类
(2) 在方法的一个作用域内定义的类
(3) 一个匿名类。用于实现一个接口
(4) 一个匿名类,用于扩展拥有非默认构建器的一个类
(5) 一个匿名类,用于运行字段初始化
(6) 一个匿名类,通过实例初始化进行构建(匿名内部类不可拥有构建器)
全部这些都在innerscopes 包内发生。首先,来自前述代码的通用接口会在它们自己的文件中获得定义,使它们能在全部的样例里使用:
//: Destination.java
package com.toad7;
interface Destination {
String readLabel();
} ///:~
因为我们已觉得Contents可能是一个抽象类,所以可採取以下这样的更自然的形式。就象一个接口那样:
//: Contents.java
package com.toad7;
interface Contents {
intvalue();
} ///:~
虽然是含有详细实施细节的一个普通类。但Wrapping 也作为它全部衍生类的一个通用“接口”使用:
//: Wrapping.java
package com.toad7;
publicclass Wrapping {
privateint i;
public Wrapping(int
x) { i = x; }
publicint value() {return
i; }
} ///:~
这里是要注意文件名称字。
在上面的代码中,我们注意到Wrapping有一个要求使用自变量的构建器,这就使情况变得更加有趣了。
怎样在一个方法的作用域(而不是还有一个类的作用域)中创建一个完整的类:
package com.toad7;
publicclass Parcel4 {
public Destination dest(Strings)
{
class PDestinationimplements
Destination {
private Stringlabel;
private PDestination(StringwhereTo) {
label =whereTo;
}
public String readLabel() {
returnlabel;
}
}
returnnew PDestination(s);
}
publicstaticvoid
main(String[] args) {
Parcel4p =new
Parcel4();
Destinationd =p.dest("Tanzania");
System.out.println(d.readLabel());
}
} // /:~
PDestination类属于 dest()的一部分,而不是 Parcel4的一部分(同一时候注意可为同样文件夹内每一个类内部的一个内部类使用类标识符 PDestination。这样做不会发生命名的冲突)。因此,PDestination不可从 dest()的外部訪问。请注意在返回语句中发生的上溯造型——除了指向基础类Destination
的一个句柄之外,没有不论什么东西超出dest()的边界之外。当然。不能因为类PDestination的名字置于 dest()内部。就觉得在dest()返回之后PDestination不是一个有效的对象。
怎样在随意作用域内嵌套一个内部类:
package com.toad7;
publicclass Parcel5 {
privatevoid internalTracking(booleanb)
{
if (b)
{
class TrackingSlip {
private Stringid;
TrackingSlip(Strings) {
id =s;
}
StringgetSlip() {
returnid;
}
}
TrackingSlipts =new
TrackingSlip("slip");
Strings =ts.getSlip();
}
// Can't use it here!Out of scope:
// ! TrackingSlipts= new TrackingSlip("x");
}
publicvoid track() {
internalTracking(true);
}
publicstaticvoid
main(String[] args) {
Parcel5p =new
Parcel5();
p.track();
}
} // /:~
TrackingSlip类嵌套于一个if语句的作用域内。
这并不意味着类是有条件创建的——它会随同其它全部东西得到编译。
然而。在定义它的那个作用域之外,它是不可使用的。
除这些以外,它看起来和一个普通类并没有什么差别。
以下这个样例看起来有些奇怪:
package com.toad7;
publicclass Parcel6 {
public Contents cont() {
returnnew Contents() {
privateinti
= 11;
publicint value() {returni;
}
}; //Semicolon required in this case
}
publicstaticvoid
main(String[] args) {
Parcel6 p =new
Parcel6();
Contents
c =p.cont();
}
} ///:~
cont()方法同一时候合并了返回值的创建代码。以及用于表示那个返回值的类。
除此以外,这个类是匿名的——它没有名字。
并且看起来似乎更让人摸不着头脑的是,我们准备创建一个 Contents 对象: return new Contents()
但在这之后,在遇到分号之前,我们又说:“等一等,让我先在一个类定义里再耍一下花招”:
这样的奇怪的语法要表达的意思是:“创建从Contents 衍生出来的匿名类的一个对象”。由new表达式返回的句柄会自己主动上溯造型成一个Contents 句柄。
匿名内部类的语法事实上要表达的是:
class MyContents extends Contents {
private int i = 11;
public int value() { return i; }
}
return new MyContents();
在匿名内部类中,Contents是用一个默认构建器创建的。以下这段代码展示了基础类须要含有自变量的一个构建器时做的事情:
package com.toad7;
publicclass Parcel7 {
public Wrapping wrap(intx)
{
// Baseconstructor call:
returnnew Wrapping(x)
{
publicint value() {
returnsuper.value() * 47;
}
}; //Semicolon required
}
publicstaticvoid
main(String[] args) {
Parcel7 p =new
Parcel7();
Wrapping
w =p.wrap(10);
}
} ///:~
将适当的自变量简单地传递给基础类构建器,在这儿表现为在“newWrapping(x)”中传递x。匿名类不能拥有一个构建器,这和在调用super()时的常规做法不同。
在前述的两个样例中,分号并不标志着类主体的结束(和 C++不同)。相反。它标志着用于包括匿名类的那个表达式的结束。因此。它全然等价于在其它不论什么地方使用分号。
若想对匿名内部类的一个对象进行某种形式的初始化,此时会出现什么情况呢?因为它是匿名的,没有名字赋给构建器。所以我们不能拥有一个构建器。
可在定义自己的字段时进行初始化:
package com.toad7;
publicclass Parcel8 {
//Argument must be final to use inside
//anonymous inner class:
public Destination dest(final
String dest) {
returnnew Destination() {
private Stringlabel
=dest;
public String readLabel() {returnlabel;
}
};
}
publicstaticvoid
main(String[] args) {
Parcel8 p =new
Parcel8();
Destination
d =p.dest("Tanzania");
}
} ///:~
若试图定义一个匿名内部类,并想使用在匿名内部类外部定义的一个对象,则编译器要求外部对象为final属性。这正是我们将dest()的自变量设为final 的原因。假设忘记这样做,就会得到一条编译期出错提示。
仅仅要自己仅仅是想分配一个字段。上述方法就肯定可行。但假如须要採取一些类似于构建器的行动,又应如何操作呢?通过Java 1.1 的实例初始化。我们能够有效地为一个匿名内部类创建一个构建器:
package com.toad7;
publicclassParcel9 {
public Destination dest(final
String dest,finalfloatprice)
{
returnnew Destination() {
privateintcost;
// Instanceinitialization for each object:
{
cost = Math.round(price);
if (cost > 100)
System.out.println("Overbudget!");
}
private Stringlabel =dest;
public String readLabel() {
returnlabel;
}
};
}
publicstaticvoid
main(String[] args) {
Parcel9p =new
Parcel9();
Destinationd =p.dest("Tanzania",
101.395F);
}
} // /:~
在实例初始化模块中,我们可看到代码不能作为类初始化模块(即if语句)的一部分运行。所以实际上,一个实例初始化模块就是一个匿名内部类的构建器。当然。它的功能是有限的;我们不能对实例初始化模块进行过载处理,所以仅仅能拥有这些构建器的当中一个。
6.3 链接到外部类
到此我们见到的内部类好象不过一种名字隐藏以及代码组织方案。虽然这些功能很实用。但似乎并不特别引人注目。然而,我们还忽略了还有一个重要的事实。创建自己的内部类时,那个类的对象同一时候拥有指向封装对象(这些对象封装或生成了内部类)的一个链接。所以它们能訪问那个封装对象的成员——毋需取得不论什么资格。除此以外,内部类拥有对封装类全部元素的訪问权限(与C++“嵌套类”的设计颇有不同,后者不过一种单纯的名字隐藏机制。在 C++中,没有指向一个封装对象的链接,也不存在默认的訪问权限。
)。以下这个样例阐示了这个问题:
package com.toad7;
interface Selector {
boolean end();
Objectcurrent();
void next();
}
publicclass Sequence {
private Object[]o;
privateintnext
= 0;
public Sequence(intsize)
{
o =new
Object[size];
}
publicvoid add(Objectx)
{
if (next
< o.length) {
o[next] =x;
next++;
}
}
privateclass SSelectorimplements
Selector {
inti = 0;
publicboolean end() {
returni ==o.length;
}
public Object current() {
returno[i];
}
publicvoid next() {
if (i <o.length)
i++;
}
}
public Selector getSelector() {
returnnew SSelector();
}
publicstaticvoid
main(String[] args) {
Sequences =new
Sequence(10);
for (inti
= 0; i < 10;i++)
s.add(Integer.toString(i));
Selectorsl =s.getSelector();
while (!sl.end())
{
System.out.println((String)sl.current());
sl.next();
}
}
} // /:~
输出例如以下:
0
1
2
3
4
5
6
7
8
9
当中。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字段。然而。内部类能够从封装类訪问方法与字段,就象已经拥有了它们一样。这一特征对我们来说是很方便的,就象在上面的样例中看到的那样。
因此。我们如今知道一个内部类能够訪问封装类的成员。这是怎样实现的呢?内部类必须拥有对封装类的特定对象的一个引用,而封装类的作用就是创建这个内部类。
随后,当我们引用封装类的一个成员时,就利用那个(隐藏)的引用来选择那个成员。幸运的是,编译器会帮助我们照管全部这些细节。但我们如今也能够理解内部类的一个对象仅仅能与封装类的一个对象联合创建。在这个创建过程中,要求对封装类对象的句柄进行初始化。
若不能訪问那个句柄,编译器就会报错。
进行全部这些操作的时候,大多数时候都不要求程序猿的不论什么介入。
6.4 static 内部类
为正确理解 static在应用于内部类时的含义,必须记住内部类的对象默认持有创建它的那个封装类的一个对象的句柄。
然而,假如我们说一个内部类是static 的。这样的说法却是不成立的。static内部类意味着:
(1) 为创建一个 static内部类的对象,我们不须要一个外部类对象。
(2) 不能从 static内部类的一个对象中訪问一个外部类对象。
但在存在一些限制:因为static成员仅仅能位于一个类的外部级别,所以内部类不可拥有static数据或static内部类。
倘若为了创建内部类的对象而不须要创建外部类的一个对象,那么可将全部东西都设为static。为了能正常工作,同一时候也必须将内部类设为static。
例如以下所看到的:
package com.toad7;
abstractclass Contents {
abstractpublicint
value();
}
interface Destination {
StringreadLabel();
}
publicclass Parcel10 {
privatestaticclass
PContents extends Contents {
privateinti
= 11;
publicint value() {
returni;
}
}
protectedstaticclass
PDestination implements Destination {
private Stringlabel;
private PDestination(StringwhereTo)
{
label =whereTo;
}
public String readLabel() {
returnlabel;
}
}
publicstatic Destination dest(Strings)
{
returnnew PDestination(s);
}
publicstatic Contents cont() {
returnnew PContents();
}
publicstaticvoid
main(String[] args) {
Contentsc =cont();
Destinationd =dest("Tanzania");
}
} // /:~
在main()中。我们不须要Parcel10 的对象。相反。我们用常规的语法来选择一个 static 成员,以便调用将句柄返回Contents 和Destination的方法。
通常。我们不在一个接口里设置不论什么代码。但 static内部类能够成为接口的一部分。
因为类是“静态”的。所以它不会违反接口的规则——static 内部类仅仅位于接口的命名空间内部:
interface IInterface {
static class Inner {
int i, j, k;
public Inner() {}
void f() {}
}
} ///:~
大家在每一个类里都设置一个main(),将其作为那个类的測试床使用。这样做的一个缺点就是额外代码的数量太多。若不愿如此。可考虑用一个static 内部类容纳自己的測试代码。例如以下所看到的:
package com.toad7;
class TestBed {
TestBed() {}
void f() { System.out.println("f()");
}
publicstaticclass
Tester {
publicstaticvoid
main(String[] args) {
TestBed
t =new TestBed();
t.f();
}
}
} ///:~
这样便生成一个独立的、名为TestBed$Tester的类(为执行程序,请使用“javaTestBed$Tester”命令)。可将这个类用于測试,但不需在自己的终于发行版本号中包括它。
6.5 引用外部类对象
若想生成外部类对象的句柄。就要用一个点号以及一个this 来命名外部类。
举个样例来说。在Sequence.SSelector 类中,它的全部方法都能产生外部类Sequence 的存储句柄,方法是採用Sequence.this的形式。
结果获得的句柄会自己主动具备正确的类型(这会在编译期间检查并核实,所以不会出现执行期的开销)。
有些时候,想告诉其它某些对象创建它某个内部类的一个对象。为达到这个目的,必须在 new表达式中提供指向其它外部类对象的一个句柄,就象以下这样:
package com.toad7;
publicclass Parcel11 {
class Contents {
privateinti
= 11;
publicint value() {
returni;
}
}
class Destination {
private Stringlabel;
Destination(StringwhereTo) {
label =whereTo;
}
StringreadLabel() {
returnlabel;
}
}
publicstaticvoid
main(String[] args) {
Parcel11p =new
Parcel11();
// Must use instanceof outer class
// to create aninstances of the inner class:
Parcel11.Contentsc =p.new
Contents();
Parcel11.Destinationd =p.new
Destination("Tanzania");
}
} // /:~
为直接创建内部类的一个对象,不能象大家也许猜想的那样——採用同样的形式,并引用外部类名Parcel11。
此时。必须利用外部类的一个对象生成内部类的一个对象:
Parcel11.Contentsc = p.new Contents();
因此,除非已拥有外部类的一个对象。否则不可能创建内部类的一个对象。
这是因为内部类的对象已同创建它的外部类的对象“默默”地连接到一起。然而,假设生成一个static 内部类,就不须要指向外部类对象的一个句柄。
6.6 从内部类继承
因为内部类构建器必须同封装类对象的一个句柄联系到一起,所以从一个内部类继承的时候。情况会略微变得有些复杂。
这儿的问题是封装类的“秘密”句柄必须获得初始化,并且在衍生类中不再有一个默认的对象能够连接。
解决问题的办法是採用一种特殊的语法,明白建立这样的关联:
package com.toad7;
class WithInner {
class Inner {}
}
publicclass InheritInner
extends WithInner.Inner {
//!InheritInner() {} // Won't compile
InheritInner(WithInnerwi) {
wi.super();
}
publicstaticvoid
main(String[] args) {
WithInner
wi =new WithInner();
InheritInner
ii =new InheritInner(wi);
}
} ///:~
从中能够看到,InheritInner仅仅对内部类进行了扩展,没有扩展外部类。
但在须要创建一个构建器的时候,默认对象已经没有意义,我们不能仅仅是传递封装对象的一个句柄。此外,必须在构建器中採用下述语法:enclosingClassHandle.super();
它提供了必要的句柄,以便程序正确编译。
6.7 内部类能够覆盖吗?
若创建一个内部类,然后从封装类继承。并又一次定义内部类。那么会出现什么情况呢?也就是说。我们有可能覆盖一个内部类吗?这看起来似乎是一个很实用的概念,但“覆盖”一个内部类——好象它是外部类的还有一个方法——这一概念实际不能做不论什么事情:
package com.toad7;
class Egg {
protectedclass Yolk {
public Yolk() {
System.out.println("Egg.Yolk()");
}
}
private Yolky;
public Egg() {
System.out.println("New Egg()");
y =new
Yolk();
}
}
publicclass BigEggextends
Egg {
publicclass Yolk {
public Yolk() {
System.out.println("BigEgg.Yolk()");
}
}
publicstaticvoid
main(String[] args) {
new BigEgg();
}
} ///:~
输出例如以下:
NewEgg()
Egg.Yolk()
默认构建器是由编译器自己主动合成的。并且会调用基础类的默认构建器。大家也许会觉得因为准备创建一个BigEgg。所以会使用Yolk 的“被覆盖”版本号。但实际情况并不是如此。
这个样例简单地揭示出当我们从外部类继承的时候,没有不论什么额外的内部类继续下去。然而。仍然有可能“明白”地从内部类继承:
package com.toad7;
class Egg2 {
protectedclass Yolk {
public Yolk() {
System.out.println("Egg2.Yolk()");
}
publicvoid f() {
System.out.println("Egg2.Yolk.f()");
}
}
private Yolky
=new Yolk();
public Egg2() {
System.out.println("NewEgg2()");
}
publicvoid insertYolk(Yolkyy)
{
y =yy;
}
publicvoid g() {
y.f();
}
}
publicclass BigEgg2extends
Egg2 {
publicclass Yolkextends
Egg2.Yolk {
public Yolk() {
System.out.println("BigEgg2.Yolk()");
}
publicvoid f() {
System.out.println("BigEgg2.Yolk.f()");
}
}
public BigEgg2() {
insertYolk(new Yolk());
}
publicstaticvoid
main(String[] args) {
Egg2e2 =new
BigEgg2();
e2.g();
}
} // /:~
如今,BigEgg2.Yolk明白地扩展了Egg2.Yolk。并且覆盖了它的方法。方法 insertYolk()同意BigEgg2将它自己的某个 Yolk 对象上溯造型至 Egg2 的y 句柄。
所以当g()调用y.f()的时候,就会使用f()被覆盖版本号。输出结果例如以下:
Egg2.Yolk()
New Egg2()
Egg2.Yolk()
BigEgg2.Yolk()
BigEgg2.Yolk.f()
对Egg2.Yolk()的第二个调用是BigEgg2.Yolk构建器的基础类构建器调用。
调用
g()的时候。可发现使用的是f()的被覆盖版本号。
6.8 内部类标识符
因为每一个类都会生成一个.class 文件,用于容纳与怎样创建这个类型的对象有关的全部信息(这样的信息产生了一个名为Class对象的元类)。所以大家也许会猜到内部类也必须生成对应的.class 文件。用来容纳与它们的Class 对象有关的信息。这些文件或类的名字遵守一种严格的形式:先是封装类的名字,再尾随一个$,再尾随内部类的名字。比如。由InheritInner.java创建的.class 文件包含:
InheritInner.class
WithInner$Inner.class
WithInner.class
假设内部类是匿名的。那么编译器会简单地生成数字,把它们作为内部类标识符使用。
若内部类嵌套于其它内部类中,则它们的名字简单地追加在一个$以及外部类标识符的后面。
这样的生成内部名称的方法除了很easy和直观以外,也很“健壮”,可适应大多数场合的要求(但在还有一方面,因为“$”也是Unix 外壳的一个元字符,所以有时会在列出.class 文件时遇到麻烦。对一家以Unix 为基础的公司——Sun——来说。採取这样的方案显得有些奇怪。我的推測是他们根本没有细致考虑这方面的问题,而是觉得我们会将所有注意力自然地放在源代码文件上。)。
因为它是Java 的标准命名机制。所以产生的文件会自己主动具备“与平台无关”的能力(注意Java
编译器会依据情况改变内部类,使其在不同的平台中能正常工作)。
6.9 为什么要用内部类:控制框架
到眼下为止,已接触了对内部类的运作进行描写叙述的大量语法与概念。但这些并不能真正说明内部类存在的原因。为什么Sun要如此麻烦地在Java1.1 里加入这种一种基本语言特性呢?答案就在于我们在这里要学习的“控制框架”。
一个“应用程序框架”是指一个或一系列类。它们专门设计用来解决特定类型的问题。为应用应用程序框架,我们可从一个或多个类继承。并覆盖当中的部分方法。
我们在覆盖方法中编写的代码用于定制由那些应用程序框架提供的常规方案。以便解决自己的实际问题。
“控制框架”属于应用程序框架的一种特殊类型。受到对事件响应的须要的支配;主要用来响应事件的一个系统叫作“由事件驱动的系统”。在应用程序设计语言中,最重要的问题之中的一个便是“图形用户界面”(GUI),它差点儿全然是由事件驱动的。
Java 1.1 AWT 属于一种控制框架,它通过内部类完美地攻克了GUI的问题。
为理解内部类怎样简化控制框架的创建与使用,可觉得一个控制框架的工作就是在事件“就绪”以后运行它们。虽然“就绪”的意思非常多,但在眼下这样的情况下,我们却是以计算机时钟为基础。随后,请认识到针对控制框架须要控制的东西,框架内并未包括不论什么特定的信息。首先,它是一个特殊的接口,描写叙述了全部控制事件。它能够是一个抽象类,而非一个实际的接口。
package com.toad7;
abstractpublicclass Event
{
privatelongevtTime;
public Event(longeventTime)
{
evtTime =eventTime;
}
publicboolean ready() {
return System.currentTimeMillis()>=evtTime;
}
abstractpublicvoid
action();
abstractpublic String description();
} ///:~
希望Event(事件)执行的时候。构建器即简单地捕获时间。同一时候 ready()告诉我们何时该执行它。
当然,ready()也能够在一个衍生类中被覆盖。将事件建立在除时间以外的其它东西上。action()是事件就绪后须要调用的方法。而 description()提供了与事件有关的文字信息。以下这个文件包括了实际的控制框架,用于管理和触发事件。
第一个类实际仅仅是一个“助手”类。它的职责是容纳Event 对象。
可用不论什么适当的集合替换它。
并且通过兴许的学习。会知道还有一些集合可简化我们的工作,不须要我们编写这些额外的代码:
EventSet 可容纳 100个事件(若在这里使用来自一个“真实”集合。就不必操心它的最大尺寸,由于它会依据情况自己主动改变大小)。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)功能(样例在《C++ Inside & Out》一书里也出现过,但 Java 提供了一种更令人舒适的解决方式。)。每一个行动都是全然不同的:控制灯光、供水以及温度自己主动调节的开与关,控制响铃,以及又一次启动系统。
但控制框架的设计宗旨是将不同的代码方便地隔离开。对每种类型的行动,都要继承一个新的Event 内部类,并在action()内编写对应的控制代码。
作为应用程序框架的一种典型行为,GreenhouseControls 类是从 Controller 继承的
package com.toad7;
publicclass GreenhouseControlsextends
Controller {
privatebooleanlight
= false;
privatebooleanwater
= false;
private Stringthermostat
="Day";
privateclass LightOnextends
Event {
public LightOn(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here to
// physically turn onthe light.
light =true;
}
public String description() {
return"Light is on";
}
}
privateclass LightOffextends
Event {
public LightOff(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here to
// physically turnoff the light.
light =false;
}
public String description() {
return"Light is off";
}
}
privateclass WaterOnextends
Event {
public WaterOn(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here
water =true;
}
public String description() {
return"Greenhouse water is on";
}
}
privateclass WaterOffextends
Event {
public WaterOff(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here
water =false;
}
public String description() {
return"Greenhouse water is off";
}
}
privateclass ThermostatNightextends
Event {
public ThermostatNight(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here
thermostat ="Night";
}
public String description() {
return"Thermostat on nightsetting";
}
}
privateclass ThermostatDayextends
Event {
public ThermostatDay(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Put hardwarecontrol code here
thermostat ="Day";
}
public String description() {
return"Thermostat on day setting";
}
}
// An example of anaction() that inserts a
// new one of itselfinto the event list:
privateintrings;
privateclass Bellextends
Event {
public Bell(longeventTime)
{
super(eventTime);
}
publicvoid action() {
// Ring bell every 2seconds, rings times:
System.out.println("Bing!");
if (--rings > 0)
addEvent(new Bell(System.currentTimeMillis()+ 2000));
}
public String description() {
return"Ring bell";
}
}
privateclass Restartextends
Event {
public Restart(longeventTime)
{
super(eventTime);
}
publicvoid action() {
longtm = System.currentTimeMillis();
// Instead ofhard-wiring, you could parse
// configurationinformation 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 aRestart object!
addEvent(new Restart(tm
+ 20000));
}
public String description() {
return"Restarting system";
}
}
publicstaticvoid
main(String[] args) {
GreenhouseControlsgc =new
GreenhouseControls();
longtm = 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负责对系统进行初始化,所以会加入全部必要的事件。当然。一种更灵活的做法是避免进行“硬编码”。而是从一个文件中读入它们。因为Restart()不过还有一个Event 对象,所以也可以在Restart.action()里加入一个 Restart 对象。使系统可以定期重新启动。
在main()中。我们须要做的所有事情就是创建一个GreenhouseControls 对象,并加入一个Restart对象。令其工作起来。
这个样例应该使对内部类的价值有一个更加深刻的认识,特别是在一个控制框架里使用它们的时候。
此外,在后半部分。会看到怎样巧妙地利用内部类描写叙述一个图形用户界面的行为。
7 构建器和多形性
构建器与其它种类的方法是有差别的。在涉及到多形性的问题后,这样的方法依旧成立。
虽然构建器并不具有多形性(即便能够使用一种“虚拟构建器”),但仍然很有必要理解构建器怎样在复杂的分级结构中以及随同多形性使用。这一理解将有助于避免陷入一些令人不快的纠纷。
7.1 构建器的调用顺序
构建器调用的顺序已在前面行了简要说明。但那是在继承和多形性问题引入之前说的话。
用于基础类的构建器肯定在一个衍生类的构建器中调用,并且逐渐向上链接。使每一个基础类使用的构建器都能得到调用。之所以要这样做,是因为构建器负有一项特殊任务:检查对象是否得到了正确的构建。
一个衍生类仅仅能訪问它自己的成员。不能訪问基础类的成员(这些成员通常都具有private 属性)。仅仅有基础类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令全部构建器都得到调用,否则整个对象的构建就可能不对。
那正是编译器为什么要强迫对衍生类的每一个部分进行构建器调用的原因。
在衍生类的构建器主体中。若我们没有明白指定对一个基础类构建器的调用,它就会“默默”地调用默认构建器。
假设不存在默认构建器。编译器就会报告一个错误(若某个类没有构建器,编译器会自己主动组织一个默认构建器)。
以下让我们看看一个样例。它展示了按构建顺序进行合成、继承以及多形性的效果:
package com.toad7;
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 Lunchextends Meal
{
Lunch(){
System.out.println("Lunch()");
}
}
class PortableLunchextends
Lunch {
PortableLunch(){
System.out.println("PortableLunch()");
}
}
class Sandwichextends
PortableLunch {
Breadb =new
Bread();
Cheesec =new
Cheese();
Lettucel =new
Lettuce();
Sandwich(){
System.out.println("Sandwich()");
}
publicstaticvoid
main(String[] args) {
new Sandwich();
}
} // /:~
这个样例在其它类的外部创建了一个复杂的类。并且每一个类都有一个构建器对自己进行了宣布。当中最重要的类是Sandwich。它反映出了三个级别的继承(若将从Object的默认继承算在内,就是四级)以及三个成员对象。
在 main()里创建了一个Sandwich 对象后。输出结果例如以下:
Meal()
Lunch()
PortableLunch()
Bread()
Cheese()
Lettuce()
Sandwich()
对于一个复杂的对象,构建器的调用遵照以下的顺序:
(1) 调用基础类构建器。这个步骤会不断反复下去,首先得到构建的是分级结构的根部,然后是下一个衍生类,等等。直到抵达最深一层的衍生类。
(2) 按声明顺序调用成员初始化模块。
(3) 调用衍生构建器的主体。
构建器调用的顺序是很重要的。进行继承时,我们知道关于基础类的一切,而且能訪问基础类的不论什么public和protected 成员。这意味着当我们在衍生类的时候,必须能假定基础类的全部成员都是有效的。採用一种标准方法,构建行动已经进行,所以对象全部部分的成员均已得到构建。
但在构建器内部,必须保证使用的全部成员都已构建。为达到这个要求。唯一的办法就是首先调用基础类构建器。
然后在进入衍生类构建器以后,我们在基础类可以訪问的全部成员都已得到初始化。此外。全部成员对象(亦即通过合成方法置于类内的对象)在类内进行定义的时候(比方上例中的b,c
和l),因为我们应尽可能地对它们进行初始化。所以也应保证构建器内部的全部成员均为有效。若坚持按这一规则行事,会有助于我们确定全部基础类成员以及当前对象的成员对象均已获得正确的初始化。
但不幸的是。这样的做法并不适用于全部情况,兴许详细说明。
7.2 继承和 finalize()
通过“合成”方法创建新类时,永远不必操心对那个类的成员对象的收尾工作。每一个成员都是一个独立的对象,所以会得到正常的垃圾收集以及收尾处理——不管它是不是不自己某个类一个成员。但在进行初始化的时候。必须覆盖衍生类中的finalize()方法——假设已经设计了某个特殊的清除进程。要求它必须作为垃圾收集的一部分进行。覆盖衍生类的 finalize()时,务必记住调用 finalize()的基础类版本号。否则,基础类的初始化根本不会发生。
以下这个样例便是明证:
package com.toad7;
class DoBaseFinalization {
publicstaticbooleanflag
= false;
}
class Characteristic {
Strings;
Characteristic(Stringc) {
s =c;
System.out.println("CreatingCharacteristic
" + s);
}
protectedvoid finalize() {
System.out.println("finalizing
Characteristic" + s);
}
}
class LivingCreature {
Characteristicp =new
Characteristic("is alive");
LivingCreature(){
System.out.println("LivingCreature()");
}
protectedvoid finalize() {
System.out.println("LivingCreaturefinalize");
// Call base-classversion LAST!
if (DoBaseFinalization.flag)
try {
super.finalize();
}catch (Throwablet)
{
}
}
}
class Animalextends
LivingCreature {
Characteristicp =new
Characteristic("has heart");
Animal(){
System.out.println("Animal()");
}
protectedvoid finalize() {
System.out.println("Animalfinalize");
if (DoBaseFinalization.flag)
try {
super.finalize();
}catch (Throwablet)
{
}
}
}
class Amphibianextends
Animal {
Characteristicp =new
Characteristic("can live inwater");
Amphibian(){
System.out.println("Amphibian()");
}
protectedvoid finalize() {
System.out.println("Amphibianfinalize");
if (DoBaseFinalization.flag)
try {
super.finalize();
}catch (Throwablet)
{
}
}
}
publicclass Frogextends
Amphibian {
Frog(){
System.out.println("Frog()");
}
protectedvoid finalize() {
System.out.println("Frogfinalize");
if (DoBaseFinalization.flag)
try {
super.finalize();
}catch (Throwablet)
{
}
}
publicstaticvoid
main(String[] args) {
if (args.length
!= 0 && args[0].equals("finalize"))
DoBaseFinalization.flag =true;
else
System.out.println("not
finalizingbases");
new Frog();// Instantly becomesgarbage
System.out.println("bye!");
// Must do this toguarantee that all
//finalizerswill be called:
System.runFinalizersOnExit(true);
}
} // /:~
DoBasefinalization 类仅仅是简单地容纳了一个标志。向分级结构中的每一个类指出是否应调用super.finalize()。这个标志的设置建立在命令行參数的基础上,所以可以在进行和不进行基础类收尾工作的前提下查看行为。
分级结构中的每一个类也包括了Characteristic 类的一个成员对象。
大家能够看到。不管是否调用了基础类收尾模块,Characteristic成员对象都肯定会得到收尾(清除)处理。
每一个被覆盖的finalize()至少要拥有对protected成员的訪问权力,由于 Object 类中的finalize()方法具有protected 属性。而编译器不同意我们在继承过程中消除訪问权限(“友好的”比“受到保护的”具有更小的訪问权限)。
在Frog.main()中,DoBaseFinalization 标志会得到配置,并且会创建单独一个Frog 对象。请记住垃圾收集(特别是收尾工作)可能不会针对不论什么特定的对象发生。所以为了强制採取这一行动。System.runFinalizersOnExit(true)加入了额外的开销。以保证收尾工作的正常进行。若没有基础类初始化,则输出结果是:
not finalizing bases
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
能够看出确实没有为基础类Frog调用收尾模块。但假如在命令行增加“finalize”自变量,则会获得下述结果:
Creating Characteristic is alive
LivingCreature()
Creating Characteristic has heart
Animal()
Creating Characteristic can live in water
Amphibian()
Frog()
bye!
Frog finalize
Amphibian finalize
Animal finalize
LivingCreature finalize
finalizing Characteristic is alive
finalizing Characteristic has heart
finalizing Characteristic can live in water
虽然成员对象依照与它们创建时同样的顺序进行收尾,但从技术角度说,并没有指定对象收尾的顺序。但对于基础类,我们可对收尾的顺序进行控制。
採用的最佳顺序正是在这里採用的顺序。它与初始化顺序正好相反。依照与 C++中用于“破坏器”同样的形式,我们应该首先运行衍生类的收尾,再是基础类的收尾。
这是因为衍生类的收尾可能调用基础类中同样的方法,要求基础类组件仍然处于活动状态。因此,必须提前将它们清除(破坏)。
7.3 构建器内部的多形性方法的行为
构建器调用的分级结构(顺序)为我们带来了一个有趣的问题,或者说让我们进入了一种进退两难的局面。
若当前位于一个构建器的内部,同一时候调用准备构建的那个对象的一个动态绑定方法,那么会出现什么情况呢?在原始的方法内部,我们全然能够想象会发生什么——动态绑定的调用会在执行期间进行解析,由于对象不知道它究竟从属于方法所在的那个类。还是从属于从它衍生出来的某些类。为保持一致性,大家或许会觉得这应该在构建器内部发生。
但实际情况并不是全然如此。
若调用构建器内部一个动态绑定的方法。会使用那个方法被覆盖的定义。然而,产生的效果可能并不如我们所愿,并且可能造成一些难于发现的程序错误。
从概念上讲。构建器的职责是让对象实际进入存在状态。在不论什么构建器内部,整个对象可能仅仅是得到部分组织——我们仅仅知道基础类对象已得到初始化,但却不知道哪些类已经继承。然而。一个动态绑定的方法调用却会在分级结构里“向前”或者“向外”前进。它调用位于衍生类里的一个方法。假设在构建器内部做这件事情,那么对于调用的方法,它要操纵的成员可能尚未得到正确的初始化——这显然不是我们所希望的。
通过观察以下这个样例,这个问题便会昭然若揭:
package com.toad7;
abstractclass Glyph {
abstractvoid draw();
Glyph(){
System.out.println("Glyph()
beforedraw()");
draw();
System.out.println("Glyph()
afterdraw()");
}
}
class RoundGlyphextends
Glyph {
intradius = 1;
RoundGlyph(intr) {
radius =r;
System.out.println("RoundGlyph.RoundGlyph(),radius
= "+ radius);
}
void draw() {
System.out.println("RoundGlyph.draw(),radius
= "+ radius);
}
}
publicclass PolyConstructors {
publicstaticvoid
main(String[] args) {
new RoundGlyph(5);
}
} // /:~
在Glyph 中,draw()方法是“抽象的”(abstract),所以它能够被其它方法覆盖。其实。我们在RoundGlyph中不得不正确其进行覆盖。
但Glyph构建器会调用这种方法,并且调用会在RoundGlyph.draw()中止,这看起来似乎是有意的。但请看看输出结果:
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
当Glyph 的构建器调用draw()时,radius 的值甚至不是默认的初始值1,而是 0。
这可能是因为一个点号或者屏幕上根本什么都没有画而造成的。这样就不得不開始查找程序中的错误。试着找出程序不能工作的原因。
前面讲述的初始化顺序并不十分完整,而那是解决这个问题的关键所在。初始化的实际过程是这种:
(1) 在採取其它不论什么操作之前,为对象分配的存储空间初始化成二进制零。
(2) 就象前面叙述的那样,调用基础类构建器。
此时,被覆盖的draw()方法会得到调用(的确是在RoundGlyph构建器调用之前),此时会发现radius的值为 0。这是因为步骤(1)造成的。
(3) 依照原先声明的顺序调用成员初始化代码。
(4) 调用衍生类构建器的主体。
採取这些操作要求有一个前提。那就是全部东西都至少要初始化成零(或者某些特殊数据类型与“零”等价的值),而不是只留作垃圾。
当中包含通过“合成”技术嵌入一个类内部的对象句柄。假设假若忘记初始化那个句柄,就会在执行期间出现违例事件。其它全部东西都会变成零,这在观看结果时一般是一个严重的警告信号。
在还有一方面,应对这个程序的结果提高警惕。从逻辑的角度说。我们似乎已进行了无懈可击的设计,所以它的错误行为令人非常不可思议。
并且没有从编译器那里收到不论什么报错信息(C++在这种情况下会表现出更合理的行为)。
象这种错误会非常轻易地被人忽略。并且要花非常长的时间才干找出。
因此,设计构建器时一个特别有效的规则是:用尽可能简单的方法使对象进入就绪状态;假设可能,避免调用不论什么方法。在构建器内唯一可以安全调用的是在基础类中具有final属性的那些方法(也适用于private方法。它们自己主动具有final属性)。
这些方法不能被覆盖,所以不会出现上述潜在的问题。
8 通过继承进行设计
学习了多形性的知识后,因为多形性是如此“聪明”的一种工具。所以看起来似乎全部东西都应该继承。
但假如过度使用继承技术,也会使自己的设计变得不必要地复杂起来。其实,当我们以一个现成类为基础建立一个新类时。如首先选择继承。会使情况变得异常复杂。
一个更好的思路是首先选择“合成”——假设不能十分确定自己应使用哪一个。合成不会强迫我们的程序设计进入继承的分级结构中。同一时候。合成显得更加灵活,由于能够动态选择一种类型(以及行为),而继承要求在编译期间准确地知道一种类型。
以下这个样例对此进行了阐释:
package com.toad7;
interface Actor {
void act();
}
class HappyActorimplements
Actor {
publicvoid act() {
System.out.println("HappyActor");
}
}
class SadActorimplements
Actor {
publicvoid act() {
System.out.println("SadActor");
}
}
class Stage {
Actora =new
HappyActor();
void change() {
a =new
SadActor();
}
void go() {
a.act();
}
}
publicclass Transmogrify {
publicstaticvoid
main(String[] args) {
Stages =new
Stage();
s.go();// Prints "HappyActor"
s.change();
s.go();// Prints "SadActor"
}
} // /:~
在这里。一个Stage 对象包括了指向一个Actor 的句柄。后者被初始化成一个 HappyActor对象。这意味着go()会产生特定的行为。但因为句柄在执行期间能够又一次与一个不同的对象绑定或结合起来。所以SadActor对象的句柄可在a 中得到替换,然后由go()产生的行为发生改变。
这样一来,我们在执行期间就获得了非常大的灵活性。与此相反,我们不能在执行期间换用不同的形式来进行继承;它要求在编译期间全然决定下来。
一条常规的设计准则是:用继承表达行为间的差异,并用成员变量表达状态的变化。
在上述样例中,两者都得到了应用:继承了两个不同的类。用于表达 act()方法的差异。而Stage通过合成技术同意它自己的状态发生变化。在这样的情况下。那种状态的改变同一时候也产生了行为的变化。
8.1 纯继承与扩展
学习继承时。为了创建继承分级结构,看来最明显的方法是採取一种“纯粹”的手段。也就是说,仅仅有在基础类或“接口”中已建立的方法才可在衍生类中被覆盖。如以下这张图所看到的:
可将其描写叙述成一种纯粹的“属于”关系,由于一个类的接口已规定了它究竟“是什么”或者“属于什么”。
通过继承。可保证全部衍生类都仅仅拥有基础类的接口。假设按上述示意图操作,衍生出来的类除了基础类的接口之外,也不会再拥有其它什么。
可将其想象成一种“纯替换”。由于衍生类对象可为基础类完美地替换掉。使用它们的时候。我们根本不是必需知道与子类有关的不论什么额外信息。例如以下所看到的:
也就是说。基础类可接收我们发给衍生类的不论什么消息。由于两者拥有全然一致的接口。我们要做的所有事情就是从衍生上溯造型,并且永远不须要回过头来检查对象的准确类型是什么。
所有细节都已通过多形性获得了完美的控制。
若按这样的思路考虑问题。那么一个纯粹的“属于”关系似乎是唯一明智的设计方法,其它不论什么设计方法都会导致混乱不清的思路,并且在定义上存在非常大的困难。
但这样的想法又属于还有一个极端。经过仔细的研究。我们发现扩展接口对于一些特定问题来说是特别有效的方案。可将其称为“类似于”关系。由于扩展后的衍生类“类似于”基础类——它们有同样的基础接口——但它添加了一些特性,要求用额外的方法加以实现。
例如以下所看到的:
虽然这是一种实用和明智的做法(由详细的环境决定),但它也有一个缺点:衍生类中对接口扩展的那一部分不可在基础类中使用。所以一旦上溯造型。就不可再调用新方法:
若在此时不进行上溯造型,则不会出现此类问题。但在很多情况下。都须要又一次核实对象的准确类型。使自己能訪问那个类型的扩展方法。
8.2 下溯造型与执行期类型标识
因为我们在上溯造型(在继承结构中向上移动)期间丢失了详细的类型信息。所以为了获取详细的类型信息——亦即在分级结构中向下移动——我们必须使用“下溯造型”技术。然而。我们知道一个上溯造型肯定是安全的。基础类不可能再拥有一个比衍生类更大的接口。因此,我们通过基础类接口发送的每一条消息都肯定可以接收到。但在进行下溯造型的时候,我们(举个样例来说)并不真的知道一个几何形状实际是一个圆。它全然可能是一个三角形、方形或者其它形状。
为解决问题,必须有一种办法可以保证下溯造型正确进行。
仅仅有这样,我们才不会冒然造型成一种错误的类型。然后发出一条对象不可能收到的消息。
这样做是很不安全的。
在某些语言中(如C++),为了进行保证“类型安全”的下溯造型。必须採取特殊的操作。
但在 Java中,全部造型都会自己主动得到检查和核实!所以即使我们仅仅是进行一次普通的括弧造型。进入执行期以后。仍然会毫无留情地对这个造型进行检查,保证它的确是我们希望的那种类型。
假设不是,就会得到一个ClassCastException(类造型违例)。
在执行期间对类型进行检查的行为叫作“执行期类型标识”(RTTI)。
以下这个样例向大家演示了RTTI的行为:
package com.toad7;
importjava.util.*;
class Useful {
publicvoid f() {
}
publicvoid g() {
}
}
class MoreUsefulextends
Useful {
publicvoid f() {
}
publicvoid g() {
}
publicvoid u() {
}
publicvoid v() {
}
publicvoid w() {
}
}
publicclass RTTI {
publicstaticvoid
main(String[] args) {
Useful[]x = {new
Useful(),new MoreUseful() };
x[0].f();
x[1].g();
// Compile-time:method not found in Useful:
// ! x[1].u();
((MoreUseful)x[1]).u();// Downcast/RTTI
((MoreUseful)x[0]).u();// Exception thrown
}
} // /:~
和在示意图中一样,MoreUseful(更实用的)对Useful(实用的)的接口进行了扩展。
但因为它是继承来的,所以也能上溯造型到一个Useful。
我们可看到这会在对数组x(位于 main()中)进行初始化的时候发生。因为数组中的两个对象都属于 Useful类。所以可将f()和g()方法同一时候发给它们两个。并且假如试图调用u()(它仅仅存在于MoreUseful)。就会收到一条编译期出错提示。
若想訪问一个MoreUseful对象的扩展接口。可试着进行下溯造型。
假设它是正确的类型,这一行动就会成功。否则,就会得到一个ClassCastException。
我们不必为这个违例编写不论什么特殊的代码,由于它指出的是一个可能在程序中不论什么地方发生的一个编程错误。
RTTI 的意义远不只反映在造型处理上。
比如,在试图下溯造型之前,可通过一种方法了解自己处理的是什么类型。
9 总结
“多形性”意味着“不同的形式”。在面向对象的程序设计中。我们有同样的外观(基础类的通用接口)以及使用那个外观的不同形式:动态绑定或组织的、不同版本号的方法。
假如不利用数据抽象以及继承技术,就不可能理解、甚至去创建多形性的一个样例。
多形性是一种不可独立应用的特性(就象一个switch 语句)。仅仅可与其它元素协同使用。我们应将其作为类整体关系的一部分来看待。人们常常混淆 Java 其它的、非面向对象的特性,比方方法过载等,这些特性有时也具有面向对象的某些特征。
但不要被愚弄:假设以后没有绑定。就不成其为多形性。
为使用多形性乃至面向对象的技术。特别是在自己的程序中,必须将自己的编程视野扩展到不仅包含单独一个类的成员和消息。也要包含类与类之间的一致性以及它们的关系。虽然这要求学习时付出很多其它的精力,但却是很值得的,由于仅仅有这样才可真正有效地加快自己的编程速度、更好地组织代码、更easy做出包容面广的程序以及更易对自己的代码进行维护与扩展。
9.JAVA编程思想 多形性的更多相关文章
- 《Java编程思想》阅读笔记二
Java编程思想 这是一个通过对<Java编程思想>(Think in java)进行阅读同时对java内容查漏补缺的系列.一些基础的知识不会被罗列出来,这里只会列出一些程序员经常会忽略或 ...
- JAVA编程思想(第四版)学习笔记----4.8 switch(知识点已更新)
switch语句和if-else语句不同,switch语句可以有多个可能的执行路径.在第四版java编程思想介绍switch语句的语法格式时写到: switch (integral-selector) ...
- 《Java编程思想》学习笔记(二)——类加载及执行顺序
<Java编程思想>学习笔记(二)--类加载及执行顺序 (这是很久之前写的,保存在印象笔记上,今天写在博客上.) 今天看Java编程思想,看到这样一道代码 //: OrderOfIniti ...
- #Java编程思想笔记(一)——static
Java编程思想笔记(一)--static 看<Java编程思想>已经有一段时间了,一直以来都把笔记做在印象笔记上,今天开始写博客来记录. 第一篇笔记来写static关键字. static ...
- [Java编程思想-学习笔记]第3章 操作符
3.1 更简单的打印语句 学习编程语言的通许遇到的第一个程序无非打印"Hello, world"了,然而在Java中要写成 System.out.println("He ...
- Java编程思想重点笔记(Java开发必看)
Java编程思想重点笔记(Java开发必看) Java编程思想,Java学习必读经典,不管是初学者还是大牛都值得一读,这里总结书中的重点知识,这些知识不仅经常出现在各大知名公司的笔试面试过程中,而 ...
- 《java编程思想》读书笔记(一)开篇&第五章(1)
2017 ---新篇章 今天终于找到阅读<java编程思想>这本书方法了,表示打开了一个新世界. 第一章:对象导论 内容不多但也有20页,主要是对整本书的一个概括.因为已经有过完整JAV ...
- Java编程思想——初始化与清理
PS:最近一直忙于项目开发..所以一直没有写博客..趁着空闲期间来一发.. 学习内容: 1.初始化 2.清理 1.初始化 虽然自己的Java基础还是比较良好的..但是在解读编程思想的时候还是发现了 ...
- java编程思想-复用类总结
今天继续读<java 编程思想>,读到了复用类一章,看到总结写的很好,现贴上来,给大家分享. 继承和组合都能从现有类型生成新类型.组合一般是将现有类型作为新类型底层实现的一部分来加以复用, ...
随机推荐
- Windows键盘驱动结构与消息机制--转
https://www.douban.com/note/318793892/ 本文主要介绍按键消息是如何传递到窗口并转化为具体的按键消息的. Windows系统是事件驱动的多任务系统,其中按键和鼠标是 ...
- Android 微信分享不出去?四步搞定!
现在做的项目中集成了友盟分享,产品要求集成微信.朋友圈.QQ.QQ空间.短信这几个分享平台.按照友盟的文档集成一切都很顺利,集成成功以后测试QQ.QQ空间.短信都没有问题,唯独微信和朋友圈一直分享不出 ...
- Win32基础知识整理
1.定义字符串 在资源新建String table,增加新字符串: (win32加载) TCHAR tcIDName[255]=_T(""); LoadString(hInstan ...
- 01--SQLite学习网站参考
1. SQLite基本操作 见我的02—SQLite一步到位 sqlite3编程笔记 . http://blog.csdn.net/wl_haanel/article/details/623141 ...
- java与javascript之间json格式数据互转
javascript中对象与字符串的互转 对象转为字符串:通过JSON.encode方法,这个是json.js里面的方法,引入到当前文件就可以了. 字符串转换为对象:①使用JSON.decode方法, ...
- Centos7搭建nginx并提供外网访问
搭建nginx之后,80端口,其他机器无法访问 查询端口是否开启 firewall-cmd --query-port=80/tcp 永久开放80端口 firewall-cmd --permanent ...
- TCP:三次握手,URG、ACK、PSH、RST、SYN、FIN 含义
http://blog.csdn.net/wudiyi815/article/details/8505726 TCP:SYN ACK FIN RST PSH URG简析 三次握手Three-way ...
- C# 时间对比
public bool IfTime(string StartTime, string EndTime) { DateTime dt1 = Convert.ToDateTime(StartTime); ...
- Ubuntu 常用解压与压缩命令
参考 https://blog.csdn.net/songbinxu/article/details/80435665 示例: 把/home/wangju/gitlab/automationTest目 ...
- EF入门
1.(安装EF)右键项目