程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 9.JAVA編程思想 多形性

9.JAVA編程思想 多形性

編輯:JAVA綜合教程

9.JAVA編程思想 多形性


“對於面向對象的程序設計語言,多型性是第三種最基本的特征(前兩種是數據抽象和繼承。”

“多形性”(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 裡綁定的所有方法都通過後期綁定具有多形性以後,就可以相應地編寫自己的代碼,令其與基礎類溝通。此時,所有的衍生類都保證能用相同的代碼正常地工作。或者換用另一種方法,我們可以“將一條消息發給一個對象,讓對象自行判斷要做什麼事情。”

在面向對象的程序設計中,有一個經典的“形狀”例子。由於它很容易用可視化的形式表現出來,所以經常都用它說明問題。但很不幸的是,它可能誤導初學者認為 OOP只是為圖形化編程設計的,這種認識當然是錯誤的。

形狀例子有一個基礎類,名為Shape;另外還有大量衍生類型:Circle(圓形),Square(方形),Triangle(三角形)等等。大家之所以喜歡這個例子,因為很容易理解“圓屬於形狀的一種類型”等概念。

下面這幅繼承圖向我們展示了它們的關系:

 

上溯造型可用下面這個語句簡單地表現出來:

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[i] =randShape();

// Makepolymorphicmethod calls:

for (inti = 0; 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

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(抽象)。否則,編譯器會向我們報告一條出錯消息。若一個抽象類是不完整的,那麼一旦有人試圖生成那個類的一個對象,編譯器又會采取什麼行動呢?由於不

能安全地為一個抽象類創建屬於它的對象,所以會從編譯器那裡獲得一條出錯提示。通過這種方法,編譯器可保證抽象類的“純潔性”,我們不必擔心會誤用它。

如果從一個抽象類繼承,而且想生成新類型的一個對象,就必須為基礎類中的所有抽象方法提供方法定義。如果不這樣做(完全可以選擇不做),則衍生類也會是抽象的,而且編譯器會強迫我們用abstract關鍵字標志那個類的“抽象”本質。

即使不包括任何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

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”(接口)關鍵字使抽象的概念更深入了一層。我們可將其想象為一個“純”抽象類。它允許創建者規定一個類的基本形式:方法名、自變量列表以及返回類型,但不規定方法主體。接口也包含了基本數據類型的數據成員,但它們都默認為static 和final。接口只提供一種形式,並不提供實施的細節。

接口這樣描述自己:“對於實現我的所有類,看起來都應該象我現在這個樣子”。因此,采用了一個特定接口的所有代碼都知道對於那個接口可能會調用什麼方法。這便是接口的全部含義。所以我們常把接口用於建立類和類之間的一個“協議”。有些面向對象的程序設計語言采用了一個名為“protocol”(協議)的關鍵字,它做的便是與接口相同的事情。

為創建一個接口,請使用interface關鍵字,而不要用 class。與類相似,我們可在 interface關鍵字的前面增加一個 public關鍵字(但只有接口定義於同名的一個文件內);或者將其省略,營造一種“友好的”狀態。

為了生成與一個特定的接口(或一組接口)相符的類,要使用implements(實現)關鍵字。我們要表達的意思是“接口看起來就象那個樣子,這兒是它具體的工作細節”。除這些之外,我們其他的工作都與繼承極為相似。下面是樂器例子的示意圖:

 

具體實現了一個接口以後,就獲得了一個普通的類,可用標准方式對其進行擴展。

可決定將一個接口中的方法聲明明確定義為“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

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 中重演。

如下所示:

 

在一個衍生類中,我們並不一定要擁有一個抽象或具體(沒有抽象方法)的基礎類。如果確實想從一個非接口繼承,那麼只能從一個繼承。剩余的所有基本元素都必須是“接口”。我們將所有接口名置於 implements關鍵字的後面,並用逗號分隔它們。可根據需要使用多個接口,而且每個接口都會成為一個獨立的類型,可對其進行上溯造型。下面這個例子展示了一個“具體”類同幾個接口合並的情況,它最終生成了一個新類:

示例如下:

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(擴展)關鍵字。但由於接口可能由多個其他接口構成,所以在構建一個新接口時,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 方法和作用域中的內部類

至此,我們已基本理解了內部類的典型用途。對那些涉及內部類的代碼,通常表達的都是“單純”的內部類,非常簡單,且極易理解。然而,內部類的設計非常全面,不可避免地會遇到它們的其他大量用法——假若我們在一個方法甚至一個任意的作用域內創建內部類。有兩方面的原因促使我們這樣做:

(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

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

如果內部類是匿名的,那麼編譯器會簡單地生成數字,把它們作為內部類標識符使用。若內部類嵌套於其他內部類中,則它們的名字簡單地追加在一個$以及外部類標識符的後面。

這種生成內部名稱的方法除了非常簡單和直觀以外,也非常“健壯”,可適應大多數場合的要求(但在另一方面,由於“$”也是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 其他的、非面向對象的特性,比如方法過載等,這些特性有時也具有面向對象的某些特征。但不要被愚弄:如果以後沒有綁定,就不成其為多形性。

為使用多形性乃至面向對象的技術,特別是在自己的程序中,必須將自己的編程視野擴展到不僅包括單獨一個類的成員和消息,也要包括類與類之間的一致性以及它們的關系。盡管這要求學習時付出更多的精力,但卻是非常值得的,因為只有這樣才可真正有效地加快自己的編程速度、更好地組織代碼、更容易做出包容面廣的程序以及更易對自己的代碼進行維護與擴展。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved